setec_astronomy 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.rvmrc +1 -0
- data/Gemfile +3 -0
- data/Gemfile.lock +32 -0
- data/README.md +19 -0
- data/Rakefile +6 -0
- data/alfred/setec.alfredextension +0 -0
- data/bin/setec +8 -0
- data/lib/setec_astronomy.rb +15 -0
- data/lib/setec_astronomy/aes_crypt.rb +19 -0
- data/lib/setec_astronomy/cli.rb +43 -0
- data/lib/setec_astronomy/kee_pass/database.rb +44 -0
- data/lib/setec_astronomy/kee_pass/entry.rb +75 -0
- data/lib/setec_astronomy/kee_pass/entry_field.rb +51 -0
- data/lib/setec_astronomy/kee_pass/group.rb +54 -0
- data/lib/setec_astronomy/kee_pass/group_field.rb +46 -0
- data/lib/setec_astronomy/kee_pass/header.rb +89 -0
- data/lib/setec_astronomy/prompt.rb +28 -0
- data/setec_astronomy.gemspec +19 -0
- data/spec/cli_spec.rb +38 -0
- data/spec/kee_pass/database_spec.rb +45 -0
- data/spec/spec_helper.rb +6 -0
- data/spec/test_database.kdb +0 -0
- metadata +161 -0
data/.rvmrc
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
rvm 1.8.7-p249@setec_astronomy
|
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
setec_astronomy (0.1.0)
|
5
|
+
clipboard (~> 0.9)
|
6
|
+
fast-aes (~> 0.1)
|
7
|
+
highline (~> 1.6)
|
8
|
+
thor (~> 0.14)
|
9
|
+
|
10
|
+
GEM
|
11
|
+
remote: http://rubygems.org/
|
12
|
+
specs:
|
13
|
+
clipboard (0.9.9)
|
14
|
+
diff-lcs (1.1.3)
|
15
|
+
fast-aes (0.1.1)
|
16
|
+
highline (1.6.2)
|
17
|
+
rspec (2.6.0)
|
18
|
+
rspec-core (~> 2.6.0)
|
19
|
+
rspec-expectations (~> 2.6.0)
|
20
|
+
rspec-mocks (~> 2.6.0)
|
21
|
+
rspec-core (2.6.4)
|
22
|
+
rspec-expectations (2.6.0)
|
23
|
+
diff-lcs (~> 1.1.2)
|
24
|
+
rspec-mocks (2.6.0)
|
25
|
+
thor (0.14.6)
|
26
|
+
|
27
|
+
PLATFORMS
|
28
|
+
ruby
|
29
|
+
|
30
|
+
DEPENDENCIES
|
31
|
+
rspec (= 2.6.0)
|
32
|
+
setec_astronomy!
|
data/README.md
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
# setec astronomy
|
2
|
+
|
3
|
+
<img src="http://www.cyberpunkreview.com/wp-content/uploads/toomanysecrets.gif" />
|
4
|
+
|
5
|
+
# almost ready for initial release!
|
6
|
+
|
7
|
+
a `setec` command is included with two basic commands:
|
8
|
+
|
9
|
+
`setec search PATTERN -f /path/to/passwords.kdb` will search your password database and output matching entries
|
10
|
+
|
11
|
+
`setec copy ENTRY_TITLE -f /path/to/passwords.kdb` will copy the password for the entry you specify straight into the clipboard
|
12
|
+
|
13
|
+
with no options, the master password is requested on the console. add the `-g` option to have the password prompt be an applescript-based gui dialog. this is especially useful when using this library for alfred integration.
|
14
|
+
|
15
|
+
an example alfred extension is included at `alfred/setec.alfredextension` - it assumes that you have this library checked out in `$HOME/dev/setec_astronomy` and all of the required gems installed in the gemset. you need the alfred powerpack to install the extension. (it's pretty easy to take a look at the command used by the extension and modify it.) assuming you get this all set up properly, you can type "stc test entry" into alfred and--if you type the master password properly--see the test password gets copied into your clipboard.
|
16
|
+
|
17
|
+
# security warning
|
18
|
+
|
19
|
+
no attempt is made to protect the memory used by this library; there may be something we can do with libgcrypt's secure-malloc functions, but right now your master password is unencrypted in ram that could possibly be paged to disk.
|
data/Rakefile
ADDED
Binary file
|
data/bin/setec
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
require 'base64'
|
2
|
+
require 'stringio'
|
3
|
+
require 'openssl'
|
4
|
+
require 'digest/sha2'
|
5
|
+
|
6
|
+
require 'rubygems'
|
7
|
+
require 'fast-aes'
|
8
|
+
|
9
|
+
require 'setec_astronomy/kee_pass/database'
|
10
|
+
require 'setec_astronomy/kee_pass/entry'
|
11
|
+
require 'setec_astronomy/kee_pass/entry_field'
|
12
|
+
require 'setec_astronomy/kee_pass/group'
|
13
|
+
require 'setec_astronomy/kee_pass/group_field'
|
14
|
+
require 'setec_astronomy/kee_pass/header'
|
15
|
+
require 'setec_astronomy/aes_crypt'
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module SetecAstronomy
|
2
|
+
module AESCrypt
|
3
|
+
def self.decrypt(encrypted_data, key, iv, cipher_type)
|
4
|
+
aes = OpenSSL::Cipher::Cipher.new(cipher_type)
|
5
|
+
aes.decrypt
|
6
|
+
aes.key = key
|
7
|
+
aes.iv = iv unless iv.nil?
|
8
|
+
aes.update(encrypted_data) + aes.final
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.encrypt(data, key, iv, cipher_type)
|
12
|
+
aes = OpenSSL::Cipher::Cipher.new(cipher_type)
|
13
|
+
aes.encrypt
|
14
|
+
aes.key = key
|
15
|
+
aes.iv = iv unless iv.nil?
|
16
|
+
aes.update(data) + aes.final
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
require 'setec_astronomy'
|
2
|
+
require 'setec_astronomy/prompt'
|
3
|
+
|
4
|
+
require 'thor'
|
5
|
+
require 'clipboard'
|
6
|
+
|
7
|
+
module SetecAstronomy
|
8
|
+
class CLI < Thor
|
9
|
+
desc "search PATTERN", "searches the database for entries matching the pattern"
|
10
|
+
method_option :file, :type => :string, :required => true, :aliases => '-f'
|
11
|
+
method_option :gui, :type => :boolean, :aliases => '-g'
|
12
|
+
def search(pattern)
|
13
|
+
keepass = database(options[:file], options[:gui])
|
14
|
+
keepass.search(pattern).each do |match|
|
15
|
+
puts "#{match.title} - #{match.notes}"
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
desc "copy ENTRY", "copies the password for the given entry to the system clipboard"
|
20
|
+
method_option :file, :type => :string, :required => true, :aliases => '-f'
|
21
|
+
method_option :gui, :type => :boolean, :aliases => '-g'
|
22
|
+
def copy(title)
|
23
|
+
keepass = database(options[:file], options[:gui])
|
24
|
+
entry = keepass.entry(title)
|
25
|
+
resign("#{title} not found") if entry.nil?
|
26
|
+
Clipboard.copy entry.password
|
27
|
+
end
|
28
|
+
|
29
|
+
no_tasks do
|
30
|
+
def database(file, gui=false)
|
31
|
+
db = KeePass::Database.open(file)
|
32
|
+
password = gui ? Prompt.ask_password_gui : Prompt.ask_password_console
|
33
|
+
resign("Unable to unlock database... exiting") unless db.unlock password
|
34
|
+
db
|
35
|
+
end
|
36
|
+
|
37
|
+
def resign(error)
|
38
|
+
puts error
|
39
|
+
exit 1
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
module SetecAstronomy
|
2
|
+
module KeePass
|
3
|
+
class Database
|
4
|
+
|
5
|
+
attr_reader :header, :groups, :entries
|
6
|
+
|
7
|
+
def self.open(path)
|
8
|
+
self.new(File.read(path))
|
9
|
+
end
|
10
|
+
|
11
|
+
def initialize(raw_db)
|
12
|
+
@header = Header.new(raw_db[0..124])
|
13
|
+
@encrypted_payload = raw_db[124..-1]
|
14
|
+
end
|
15
|
+
|
16
|
+
def entry(title)
|
17
|
+
@entries.detect { |e| e.title == title }
|
18
|
+
end
|
19
|
+
|
20
|
+
def unlock(master_password)
|
21
|
+
@final_key = header.final_key(master_password)
|
22
|
+
decrypt_payload
|
23
|
+
payload_io = StringIO.new(@payload)
|
24
|
+
@groups = Group.extract_from_payload(header, payload_io)
|
25
|
+
@entries = Entry.extract_from_payload(header, payload_io)
|
26
|
+
true
|
27
|
+
rescue OpenSSL::Cipher::CipherError
|
28
|
+
false
|
29
|
+
end
|
30
|
+
|
31
|
+
def search(pattern)
|
32
|
+
entries.select { |e| e.title =~ /#{pattern}/ }
|
33
|
+
end
|
34
|
+
|
35
|
+
def valid?
|
36
|
+
@header.valid?
|
37
|
+
end
|
38
|
+
|
39
|
+
def decrypt_payload
|
40
|
+
@payload = AESCrypt.decrypt(@encrypted_payload, @final_key, header.encryption_iv, 'AES-256-CBC')
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
# One entry: [FIELDTYPE(FT)][FIELDSIZE(FS)][FIELDDATA(FD)]
|
2
|
+
# [FT+FS+(FD)][FT+FS+(FD)][FT+FS+(FD)][FT+FS+(FD)][FT+FS+(FD)]...
|
3
|
+
|
4
|
+
# [ 2 bytes] FIELDTYPE
|
5
|
+
# [ 4 bytes] FIELDSIZE, size of FIELDDATA in bytes
|
6
|
+
# [ n bytes] FIELDDATA, n = FIELDSIZE
|
7
|
+
|
8
|
+
# Notes:
|
9
|
+
# - Strings are stored in UTF-8 encoded form and are null-terminated.
|
10
|
+
# - FIELDTYPE can be one of the following identifiers:
|
11
|
+
# * 0000: Invalid or comment block, block is ignored
|
12
|
+
# * 0001: UUID, uniquely identifying an entry, FIELDSIZE must be 16
|
13
|
+
# * 0002: Group ID, identifying the group of the entry, FIELDSIZE = 4
|
14
|
+
# It can be any 32-bit value except 0 and 0xFFFFFFFF
|
15
|
+
# * 0003: Image ID, identifying the image/icon of the entry, FIELDSIZE = 4
|
16
|
+
# * 0004: Title of the entry, FIELDDATA is an UTF-8 encoded string
|
17
|
+
# * 0005: URL string, FIELDDATA is an UTF-8 encoded string
|
18
|
+
# * 0006: UserName string, FIELDDATA is an UTF-8 encoded string
|
19
|
+
# * 0007: Password string, FIELDDATA is an UTF-8 encoded string
|
20
|
+
# * 0008: Notes string, FIELDDATA is an UTF-8 encoded string
|
21
|
+
# * 0009: Creation time, FIELDSIZE = 5, FIELDDATA = packed date/time
|
22
|
+
# * 000A: Last modification time, FIELDSIZE = 5, FIELDDATA = packed date/time
|
23
|
+
# * 000B: Last access time, FIELDSIZE = 5, FIELDDATA = packed date/time
|
24
|
+
# * 000C: Expiration time, FIELDSIZE = 5, FIELDDATA = packed date/time
|
25
|
+
# * 000D: Binary description UTF-8 encoded string
|
26
|
+
# * 000E: Binary data
|
27
|
+
# * FFFF: Entry terminator, FIELDSIZE must be 0
|
28
|
+
# '''
|
29
|
+
|
30
|
+
module SetecAstronomy
|
31
|
+
module KeePass
|
32
|
+
class Entry
|
33
|
+
def self.extract_from_payload(header, payload_io)
|
34
|
+
groups = []
|
35
|
+
header.nentries.times do
|
36
|
+
group = Entry.new(payload_io)
|
37
|
+
groups << group
|
38
|
+
end
|
39
|
+
groups
|
40
|
+
end
|
41
|
+
|
42
|
+
attr_reader :fields
|
43
|
+
|
44
|
+
def initialize(payload_io)
|
45
|
+
fields = []
|
46
|
+
begin
|
47
|
+
field = EntryField.new(payload_io)
|
48
|
+
fields << field
|
49
|
+
end while not field.terminator?
|
50
|
+
|
51
|
+
@fields = fields
|
52
|
+
end
|
53
|
+
|
54
|
+
def length
|
55
|
+
@fields.map(&:length).reduce(&:+)
|
56
|
+
end
|
57
|
+
|
58
|
+
def notes
|
59
|
+
@fields.detect { |field| field.name == 'notes' }.data
|
60
|
+
end
|
61
|
+
|
62
|
+
def password
|
63
|
+
@fields.detect { |field| field.name == 'password' }.data.chomp("\000")
|
64
|
+
end
|
65
|
+
|
66
|
+
def title
|
67
|
+
@fields.detect { |field| field.name == 'title' }.data.chomp("\000")
|
68
|
+
end
|
69
|
+
|
70
|
+
def username
|
71
|
+
@fields.detect { |field| field.name == 'username' }.data
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
module SetecAstronomy
|
2
|
+
module KeePass
|
3
|
+
class EntryField
|
4
|
+
FIELD_TYPES = [
|
5
|
+
[0x0, 'ignored', :null],
|
6
|
+
[0x1, 'uuid', :ascii],
|
7
|
+
[0x2, 'groupid', :int],
|
8
|
+
[0x3, 'imageid', :int],
|
9
|
+
[0x4, 'title', :string],
|
10
|
+
[0x5, 'url', :string],
|
11
|
+
[0x6, 'username', :string],
|
12
|
+
[0x7, 'password', :string],
|
13
|
+
[0x8, 'notes', :string],
|
14
|
+
[0x9, 'creation_time', :date],
|
15
|
+
[0xa, 'last_mod_time', :date],
|
16
|
+
[0xb, 'last_acc_time', :date],
|
17
|
+
[0xc, 'expiration_time', :date],
|
18
|
+
[0xd, 'binary_desc', :string],
|
19
|
+
[0xe, 'binary_data', :shunt],
|
20
|
+
[0xFFFF, 'terminator', :nil]
|
21
|
+
]
|
22
|
+
FIELD_TERMINATOR = 0xFFFF
|
23
|
+
TYPE_CODE_FIELD_SIZE = 2 # unsigned short integer
|
24
|
+
DATA_LENGTH_FIELD_SIZE = 4 # unsigned integer
|
25
|
+
|
26
|
+
|
27
|
+
attr_reader :name, :data_type, :data
|
28
|
+
|
29
|
+
def initialize(payload)
|
30
|
+
type_code, @data_length = payload.read(TYPE_CODE_FIELD_SIZE + DATA_LENGTH_FIELD_SIZE).unpack('SI')
|
31
|
+
@name, @data_type = _parse_type_code(type_code)
|
32
|
+
@data = payload.read(@data_length)
|
33
|
+
end
|
34
|
+
|
35
|
+
def terminator?
|
36
|
+
name == 'terminator'
|
37
|
+
end
|
38
|
+
|
39
|
+
def length
|
40
|
+
TYPE_CODE_FIELD_SIZE + DATA_LENGTH_FIELD_SIZE + @data_length
|
41
|
+
end
|
42
|
+
|
43
|
+
def _parse_type_code(type_code)
|
44
|
+
(_, name, data_type) = FIELD_TYPES.detect do |(code, *rest)|
|
45
|
+
code == type_code
|
46
|
+
end
|
47
|
+
[name, data_type]
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
# One group: [FIELDTYPE(FT)][FIELDSIZE(FS)][FIELDDATA(FD)]
|
2
|
+
# [FT+FS+(FD)][FT+FS+(FD)][FT+FS+(FD)][FT+FS+(FD)][FT+FS+(FD)]...
|
3
|
+
#
|
4
|
+
# [ 2 bytes] FIELDTYPE
|
5
|
+
# [ 4 bytes] FIELDSIZE, size of FIELDDATA in bytes
|
6
|
+
# [ n bytes] FIELDDATA, n = FIELDSIZE
|
7
|
+
#
|
8
|
+
# Notes:
|
9
|
+
# - Strings are stored in UTF-8 encoded form and are null-terminated.
|
10
|
+
# - FIELDTYPE can be one of the following identifiers:
|
11
|
+
# * 0000: Invalid or comment block, block is ignored
|
12
|
+
# * 0001: Group ID, FIELDSIZE must be 4 bytes
|
13
|
+
# It can be any 32-bit value except 0 and 0xFFFFFFFF
|
14
|
+
# * 0002: Group name, FIELDDATA is an UTF-8 encoded string
|
15
|
+
# * 0003: Creation time, FIELDSIZE = 5, FIELDDATA = packed date/time
|
16
|
+
# * 0004: Last modification time, FIELDSIZE = 5, FIELDDATA = packed date/time
|
17
|
+
# * 0005: Last access time, FIELDSIZE = 5, FIELDDATA = packed date/time
|
18
|
+
# * 0006: Expiration time, FIELDSIZE = 5, FIELDDATA = packed date/time
|
19
|
+
# * 0007: Image ID, FIELDSIZE must be 4 bytes
|
20
|
+
# * 0008: Level, FIELDSIZE = 2
|
21
|
+
# * 0009: Flags, 32-bit value, FIELDSIZE = 4
|
22
|
+
# * FFFF: Group entry terminator, FIELDSIZE must be 0
|
23
|
+
module SetecAstronomy
|
24
|
+
module KeePass
|
25
|
+
class Group
|
26
|
+
def self.extract_from_payload(header, payload_io)
|
27
|
+
groups = []
|
28
|
+
header.ngroups.times do
|
29
|
+
group = Group.new(payload_io)
|
30
|
+
groups << group
|
31
|
+
end
|
32
|
+
groups
|
33
|
+
end
|
34
|
+
|
35
|
+
def initialize(payload_io)
|
36
|
+
fields = []
|
37
|
+
begin
|
38
|
+
field = GroupField.new(payload_io)
|
39
|
+
fields << field
|
40
|
+
end while not field.terminator?
|
41
|
+
|
42
|
+
@fields = fields
|
43
|
+
end
|
44
|
+
|
45
|
+
def length
|
46
|
+
@fields.map(&:length).reduce(&:+)
|
47
|
+
end
|
48
|
+
|
49
|
+
def name
|
50
|
+
@fields.detect { |field| field.name == 'group_name' }.data.chomp("\000")
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
module SetecAstronomy
|
2
|
+
module KeePass
|
3
|
+
class GroupField
|
4
|
+
FIELD_TYPES = [
|
5
|
+
[0x0, 'ignored', :null],
|
6
|
+
[0x1, 'groupid', :int],
|
7
|
+
[0x2, 'group_name', :string],
|
8
|
+
[0x3, 'creation_time', :date],
|
9
|
+
[0x4, 'lastmod_time', :date],
|
10
|
+
[0x5, 'lastacc_time', :date],
|
11
|
+
[0x6, 'expire_time', :date],
|
12
|
+
[0x7, 'imageid', :int],
|
13
|
+
[0x8, 'level', :short],
|
14
|
+
[0x9, 'flags', :int],
|
15
|
+
[0xFFFF, 'terminator', :null]
|
16
|
+
]
|
17
|
+
FIELD_TERMINATOR = 0xFFFF
|
18
|
+
TYPE_CODE_FIELD_SIZE = 2 # unsigned short integer
|
19
|
+
DATA_LENGTH_FIELD_SIZE = 4 # unsigned integer
|
20
|
+
|
21
|
+
|
22
|
+
attr_reader :name, :data_type, :data
|
23
|
+
|
24
|
+
def initialize(payload)
|
25
|
+
type_code, @data_length = payload.read(TYPE_CODE_FIELD_SIZE + DATA_LENGTH_FIELD_SIZE).unpack('SI')
|
26
|
+
@name, @data_type = _parse_type_code(type_code)
|
27
|
+
@data = payload.read(@data_length)
|
28
|
+
end
|
29
|
+
|
30
|
+
def terminator?
|
31
|
+
name == 'terminator'
|
32
|
+
end
|
33
|
+
|
34
|
+
def length
|
35
|
+
TYPE_CODE_FIELD_SIZE + DATA_LENGTH_FIELD_SIZE + @data_length
|
36
|
+
end
|
37
|
+
|
38
|
+
def _parse_type_code(type_code)
|
39
|
+
(_, name, data_type) = FIELD_TYPES.detect do |(code, *rest)|
|
40
|
+
code == type_code
|
41
|
+
end
|
42
|
+
[name, data_type]
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
# The keepass file header.
|
2
|
+
#
|
3
|
+
# From the KeePass doc:
|
4
|
+
#
|
5
|
+
# Database header: [DBHDR]
|
6
|
+
#
|
7
|
+
# [ 4 bytes] DWORD dwSignature1 = 0x9AA2D903
|
8
|
+
# [ 4 bytes] DWORD dwSignature2 = 0xB54BFB65
|
9
|
+
# [ 4 bytes] DWORD dwFlags
|
10
|
+
# [ 4 bytes] DWORD dwVersion { Ve.Ve.Mj.Mj:Mn.Mn.Bl.Bl }
|
11
|
+
# [16 bytes] BYTE{16} aMasterSeed
|
12
|
+
# [16 bytes] BYTE{16} aEncryptionIV
|
13
|
+
# [ 4 bytes] DWORD dwGroups Number of groups in database
|
14
|
+
# [ 4 bytes] DWORD dwEntries Number of entries in database
|
15
|
+
# [32 bytes] BYTE{32} aContentsHash SHA-256 hash value of the plain contents
|
16
|
+
# [32 bytes] BYTE{32} aMasterSeed2 Used for the dwKeyEncRounds AES
|
17
|
+
# master key transformations
|
18
|
+
# [ 4 bytes] DWORD dwKeyEncRounds See above; number of transformations
|
19
|
+
#
|
20
|
+
# Notes:
|
21
|
+
#
|
22
|
+
# - dwFlags is a bitmap, which can include:
|
23
|
+
# * PWM_FLAG_SHA2 (1) for SHA-2.
|
24
|
+
# * PWM_FLAG_RIJNDAEL (2) for AES (Rijndael).
|
25
|
+
# * PWM_FLAG_ARCFOUR (4) for ARC4.
|
26
|
+
# * PWM_FLAG_TWOFISH (8) for Twofish.
|
27
|
+
# - aMasterSeed is a salt that gets hashed with the transformed user master key
|
28
|
+
# to form the final database data encryption/decryption key.
|
29
|
+
# * FinalKey = SHA-256(aMasterSeed, TransformedUserMasterKey)
|
30
|
+
# - aEncryptionIV is the initialization vector used by AES/Twofish for
|
31
|
+
# encrypting/decrypting the database data.
|
32
|
+
# - aContentsHash: "plain contents" refers to the database file, minus the
|
33
|
+
# database header, decrypted by FinalKey.
|
34
|
+
# * PlainContents = Decrypt_with_FinalKey(DatabaseFile - DatabaseHeader)
|
35
|
+
module SetecAstronomy
|
36
|
+
module KeePass
|
37
|
+
class Header
|
38
|
+
|
39
|
+
ENCRYPTION_FLAGS = [
|
40
|
+
[1 , 'SHA2' ],
|
41
|
+
[2 , 'Rijndael'],
|
42
|
+
[2 , 'AES' ],
|
43
|
+
[4 , 'ArcFour' ],
|
44
|
+
[8 , 'TwoFish' ]
|
45
|
+
]
|
46
|
+
|
47
|
+
attr_reader :encryption_iv
|
48
|
+
attr_reader :ngroups, :nentries
|
49
|
+
|
50
|
+
def initialize(header_bytes)
|
51
|
+
@signature1 = header_bytes[0..4].unpack('L*').first
|
52
|
+
@signature2 = header_bytes[4..8].unpack('L*').first
|
53
|
+
@flags = header_bytes[8..12].unpack('L*').first
|
54
|
+
@version = header_bytes[12..16].unpack('L*').first
|
55
|
+
@master_seed = header_bytes[16...32]
|
56
|
+
@encryption_iv = header_bytes[32...48]
|
57
|
+
@ngroups = header_bytes[48..52].unpack('L*').first
|
58
|
+
@nentries = header_bytes[52..56].unpack('L*').first
|
59
|
+
@contents_hash = header_bytes[56..88]
|
60
|
+
@master_seed2 = header_bytes[88...120]
|
61
|
+
@rounds = header_bytes[120..-1].unpack('L*').first
|
62
|
+
end
|
63
|
+
|
64
|
+
def valid?
|
65
|
+
@signature1 == 0x9AA2D903 && @signature2 == 0xB54BFB65
|
66
|
+
end
|
67
|
+
|
68
|
+
def encryption_type
|
69
|
+
ENCRYPTION_FLAGS.each do |(flag_mask, encryption_type)|
|
70
|
+
return encryption_type if @flags & flag_mask
|
71
|
+
end
|
72
|
+
'Unknown'
|
73
|
+
end
|
74
|
+
|
75
|
+
def final_key(master_key)
|
76
|
+
key = Digest::SHA2.new.update(master_key).digest
|
77
|
+
aes = FastAES.new(@master_seed2)
|
78
|
+
|
79
|
+
@rounds.times do |i|
|
80
|
+
key = aes.encrypt(key)
|
81
|
+
end
|
82
|
+
|
83
|
+
key = Digest::SHA2.new.update(key).digest
|
84
|
+
key = Digest::SHA2.new.update(@master_seed + key).digest
|
85
|
+
key
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'highline/import'
|
2
|
+
|
3
|
+
module Prompt
|
4
|
+
def self.ask_password_console
|
5
|
+
ask("Password: ") { |q| q.echo = false }
|
6
|
+
end
|
7
|
+
|
8
|
+
def self.ask_password_gui
|
9
|
+
`#{_password_dialog_actionscript}`.chomp
|
10
|
+
end
|
11
|
+
|
12
|
+
def self._password_dialog_actionscript
|
13
|
+
<<-APPLESCRIPT.gsub(/^ */, '')
|
14
|
+
/usr/bin/osascript <<EOT
|
15
|
+
tell application "Finder"
|
16
|
+
activate
|
17
|
+
set output to text returned of ( \
|
18
|
+
display dialog "Enter your master password:" \
|
19
|
+
with title "KeePass Database Master Password" \
|
20
|
+
default answer "" \
|
21
|
+
with hidden answer \
|
22
|
+
with icon (path to "apps") as Unicode text & "Utilities:Keychain Access.app:Contents:Resources:Keychain.icns" as alias \
|
23
|
+
)
|
24
|
+
end tell
|
25
|
+
EOT
|
26
|
+
APPLESCRIPT
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
Gem::Specification.new do |s|
|
2
|
+
s.name = "setec_astronomy"
|
3
|
+
s.summary = "Command line and API access to KeePassX databases"
|
4
|
+
s.description = "See http://github.com/pitluga/setec_astronomy"
|
5
|
+
s.version = "0.1.0"
|
6
|
+
s.author = "Tony Pitluga"
|
7
|
+
s.email = "tony.pitluga@gmail.com"
|
8
|
+
s.homepage = "http://github.com/pitluga/supply_drop"
|
9
|
+
s.files = `git ls-files`.split("\n")
|
10
|
+
s.bindir = "bin"
|
11
|
+
s.executables = ["setec"]
|
12
|
+
|
13
|
+
s.add_dependency "clipboard", "~> 0.9"
|
14
|
+
s.add_dependency "fast-aes", "~> 0.1"
|
15
|
+
s.add_dependency "highline", "~> 1.6"
|
16
|
+
s.add_dependency "thor", "~> 0.14"
|
17
|
+
|
18
|
+
s.add_development_dependency "rspec", "2.6.0"
|
19
|
+
end
|
data/spec/cli_spec.rb
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'setec_astronomy/cli'
|
3
|
+
require 'pty'
|
4
|
+
require 'expect'
|
5
|
+
|
6
|
+
describe "Command Line" do
|
7
|
+
def setec(options, master_password = nil)
|
8
|
+
bin = File.expand_path('../../bin/setec', __FILE__)
|
9
|
+
cmd = "#{bin} #{options} --file=#{TEST_DATABASE_PATH}"
|
10
|
+
output = ''
|
11
|
+
PTY.spawn cmd do |reader, writer, pid|
|
12
|
+
reader.expect("Password:") do
|
13
|
+
unless master_password.nil?
|
14
|
+
writer.puts master_password
|
15
|
+
reader.gets
|
16
|
+
end
|
17
|
+
end
|
18
|
+
until reader.eof?
|
19
|
+
output << reader.gets
|
20
|
+
end
|
21
|
+
end
|
22
|
+
output
|
23
|
+
end
|
24
|
+
|
25
|
+
describe "search" do
|
26
|
+
it "lists the entries that contain the given text" do
|
27
|
+
output = setec 'search test', "testmasterpassword"
|
28
|
+
output.should include("test entry")
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
describe "copy" do
|
33
|
+
it "copies the given password to the system clipboard" do
|
34
|
+
setec 'copy "test entry"', "testmasterpassword"
|
35
|
+
Clipboard.paste.should == "testpassword"
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe SetecAstronomy::KeePass::Database do
|
4
|
+
describe 'self.open' do
|
5
|
+
it "creates a new instance of the databse with the file" do
|
6
|
+
db = SetecAstronomy::KeePass::Database.open(TEST_DATABASE_PATH)
|
7
|
+
db.should_not be_nil
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
describe "unlock" do
|
12
|
+
before :each do
|
13
|
+
@db = SetecAstronomy::KeePass::Database.open(TEST_DATABASE_PATH)
|
14
|
+
@db.should be_valid
|
15
|
+
end
|
16
|
+
|
17
|
+
it "returns true when the master password is correct" do
|
18
|
+
@db.unlock('testmasterpassword').should be_true
|
19
|
+
end
|
20
|
+
|
21
|
+
it "returns false when the master password is incorrect" do
|
22
|
+
@db.unlock('bad password').should be_false
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
describe "an unlocked database" do
|
27
|
+
before :each do
|
28
|
+
@db = SetecAstronomy::KeePass::Database.open(TEST_DATABASE_PATH)
|
29
|
+
@db.unlock('testmasterpassword')
|
30
|
+
end
|
31
|
+
|
32
|
+
it "can find entries by their title" do
|
33
|
+
@db.entry("test entry").password.should == "testpassword"
|
34
|
+
end
|
35
|
+
|
36
|
+
it "can find groups" do
|
37
|
+
@db.groups.map(&:name).sort.should == ["Internet", "eMail"]
|
38
|
+
end
|
39
|
+
|
40
|
+
it "can search for entries" do
|
41
|
+
entries = @db.search "test"
|
42
|
+
entries.first.title.should == "test entry"
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
data/spec/spec_helper.rb
ADDED
Binary file
|
metadata
ADDED
@@ -0,0 +1,161 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: setec_astronomy
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 27
|
5
|
+
prerelease:
|
6
|
+
segments:
|
7
|
+
- 0
|
8
|
+
- 1
|
9
|
+
- 0
|
10
|
+
version: 0.1.0
|
11
|
+
platform: ruby
|
12
|
+
authors:
|
13
|
+
- Tony Pitluga
|
14
|
+
autorequire:
|
15
|
+
bindir: bin
|
16
|
+
cert_chain: []
|
17
|
+
|
18
|
+
date: 2011-12-09 00:00:00 Z
|
19
|
+
dependencies:
|
20
|
+
- !ruby/object:Gem::Dependency
|
21
|
+
name: clipboard
|
22
|
+
prerelease: false
|
23
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
24
|
+
none: false
|
25
|
+
requirements:
|
26
|
+
- - ~>
|
27
|
+
- !ruby/object:Gem::Version
|
28
|
+
hash: 25
|
29
|
+
segments:
|
30
|
+
- 0
|
31
|
+
- 9
|
32
|
+
version: "0.9"
|
33
|
+
type: :runtime
|
34
|
+
version_requirements: *id001
|
35
|
+
- !ruby/object:Gem::Dependency
|
36
|
+
name: fast-aes
|
37
|
+
prerelease: false
|
38
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
39
|
+
none: false
|
40
|
+
requirements:
|
41
|
+
- - ~>
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
hash: 9
|
44
|
+
segments:
|
45
|
+
- 0
|
46
|
+
- 1
|
47
|
+
version: "0.1"
|
48
|
+
type: :runtime
|
49
|
+
version_requirements: *id002
|
50
|
+
- !ruby/object:Gem::Dependency
|
51
|
+
name: highline
|
52
|
+
prerelease: false
|
53
|
+
requirement: &id003 !ruby/object:Gem::Requirement
|
54
|
+
none: false
|
55
|
+
requirements:
|
56
|
+
- - ~>
|
57
|
+
- !ruby/object:Gem::Version
|
58
|
+
hash: 3
|
59
|
+
segments:
|
60
|
+
- 1
|
61
|
+
- 6
|
62
|
+
version: "1.6"
|
63
|
+
type: :runtime
|
64
|
+
version_requirements: *id003
|
65
|
+
- !ruby/object:Gem::Dependency
|
66
|
+
name: thor
|
67
|
+
prerelease: false
|
68
|
+
requirement: &id004 !ruby/object:Gem::Requirement
|
69
|
+
none: false
|
70
|
+
requirements:
|
71
|
+
- - ~>
|
72
|
+
- !ruby/object:Gem::Version
|
73
|
+
hash: 23
|
74
|
+
segments:
|
75
|
+
- 0
|
76
|
+
- 14
|
77
|
+
version: "0.14"
|
78
|
+
type: :runtime
|
79
|
+
version_requirements: *id004
|
80
|
+
- !ruby/object:Gem::Dependency
|
81
|
+
name: rspec
|
82
|
+
prerelease: false
|
83
|
+
requirement: &id005 !ruby/object:Gem::Requirement
|
84
|
+
none: false
|
85
|
+
requirements:
|
86
|
+
- - "="
|
87
|
+
- !ruby/object:Gem::Version
|
88
|
+
hash: 23
|
89
|
+
segments:
|
90
|
+
- 2
|
91
|
+
- 6
|
92
|
+
- 0
|
93
|
+
version: 2.6.0
|
94
|
+
type: :development
|
95
|
+
version_requirements: *id005
|
96
|
+
description: See http://github.com/pitluga/setec_astronomy
|
97
|
+
email: tony.pitluga@gmail.com
|
98
|
+
executables:
|
99
|
+
- setec
|
100
|
+
extensions: []
|
101
|
+
|
102
|
+
extra_rdoc_files: []
|
103
|
+
|
104
|
+
files:
|
105
|
+
- .rvmrc
|
106
|
+
- Gemfile
|
107
|
+
- Gemfile.lock
|
108
|
+
- README.md
|
109
|
+
- Rakefile
|
110
|
+
- alfred/setec.alfredextension
|
111
|
+
- bin/setec
|
112
|
+
- lib/setec_astronomy.rb
|
113
|
+
- lib/setec_astronomy/aes_crypt.rb
|
114
|
+
- lib/setec_astronomy/cli.rb
|
115
|
+
- lib/setec_astronomy/kee_pass/database.rb
|
116
|
+
- lib/setec_astronomy/kee_pass/entry.rb
|
117
|
+
- lib/setec_astronomy/kee_pass/entry_field.rb
|
118
|
+
- lib/setec_astronomy/kee_pass/group.rb
|
119
|
+
- lib/setec_astronomy/kee_pass/group_field.rb
|
120
|
+
- lib/setec_astronomy/kee_pass/header.rb
|
121
|
+
- lib/setec_astronomy/prompt.rb
|
122
|
+
- setec_astronomy.gemspec
|
123
|
+
- spec/cli_spec.rb
|
124
|
+
- spec/kee_pass/database_spec.rb
|
125
|
+
- spec/spec_helper.rb
|
126
|
+
- spec/test_database.kdb
|
127
|
+
homepage: http://github.com/pitluga/supply_drop
|
128
|
+
licenses: []
|
129
|
+
|
130
|
+
post_install_message:
|
131
|
+
rdoc_options: []
|
132
|
+
|
133
|
+
require_paths:
|
134
|
+
- lib
|
135
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
136
|
+
none: false
|
137
|
+
requirements:
|
138
|
+
- - ">="
|
139
|
+
- !ruby/object:Gem::Version
|
140
|
+
hash: 3
|
141
|
+
segments:
|
142
|
+
- 0
|
143
|
+
version: "0"
|
144
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
145
|
+
none: false
|
146
|
+
requirements:
|
147
|
+
- - ">="
|
148
|
+
- !ruby/object:Gem::Version
|
149
|
+
hash: 3
|
150
|
+
segments:
|
151
|
+
- 0
|
152
|
+
version: "0"
|
153
|
+
requirements: []
|
154
|
+
|
155
|
+
rubyforge_project:
|
156
|
+
rubygems_version: 1.8.10
|
157
|
+
signing_key:
|
158
|
+
specification_version: 3
|
159
|
+
summary: Command line and API access to KeePassX databases
|
160
|
+
test_files: []
|
161
|
+
|