setec_astronomy 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|
+
|