keepassx 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/.travis.yml +5 -0
- data/Gemfile +5 -0
- data/Gemfile.lock +28 -0
- data/README.md +0 -0
- data/Rakefile +5 -0
- data/keepassx.gemspec +14 -0
- data/lib/keepassx.rb +13 -0
- data/lib/keepassx/aes_crypt.rb +19 -0
- data/lib/keepassx/database.rb +45 -0
- data/lib/keepassx/entry.rb +81 -0
- data/lib/keepassx/entry_field.rb +49 -0
- data/lib/keepassx/group.rb +56 -0
- data/lib/keepassx/group_field.rb +44 -0
- data/lib/keepassx/header.rb +87 -0
- data/spec/keepassx/database_spec.rb +56 -0
- data/spec/spec_helper.rb +5 -0
- data/spec/test_database.kdb +0 -0
- metadata +108 -0
data/.rvmrc
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
rvm 1.8.7-p249@keepassx --create
|
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
keepassx (0.1.0)
|
5
|
+
fast-aes (~> 0.1)
|
6
|
+
|
7
|
+
GEM
|
8
|
+
remote: http://rubygems.org/
|
9
|
+
specs:
|
10
|
+
diff-lcs (1.1.3)
|
11
|
+
fast-aes (0.1.1)
|
12
|
+
rake (0.8.7)
|
13
|
+
rspec (2.11.0)
|
14
|
+
rspec-core (~> 2.11.0)
|
15
|
+
rspec-expectations (~> 2.11.0)
|
16
|
+
rspec-mocks (~> 2.11.0)
|
17
|
+
rspec-core (2.11.1)
|
18
|
+
rspec-expectations (2.11.3)
|
19
|
+
diff-lcs (~> 1.1.3)
|
20
|
+
rspec-mocks (2.11.3)
|
21
|
+
|
22
|
+
PLATFORMS
|
23
|
+
ruby
|
24
|
+
|
25
|
+
DEPENDENCIES
|
26
|
+
keepassx!
|
27
|
+
rake (= 0.8.7)
|
28
|
+
rspec (= 2.11.0)
|
data/README.md
ADDED
File without changes
|
data/Rakefile
ADDED
data/keepassx.gemspec
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
Gem::Specification.new do |s|
|
2
|
+
s.name = "keepassx"
|
3
|
+
s.summary = "Ruby API access for KeePassX databases"
|
4
|
+
s.description = "See http://github.com/pitluga/keepassx"
|
5
|
+
s.version = "0.1.0"
|
6
|
+
s.authors = ["Tony Pitluga", "Paul Hinze"]
|
7
|
+
s.email = ["tony.pitluga@gmail.com", "paul.t.hinze@gmail.com"]
|
8
|
+
s.homepage = "http://github.com/pitluga/keepassx"
|
9
|
+
s.files = `git ls-files`.split("\n")
|
10
|
+
|
11
|
+
s.add_dependency "fast-aes", "~> 0.1"
|
12
|
+
|
13
|
+
s.add_development_dependency "rspec", "2.11.0"
|
14
|
+
end
|
data/lib/keepassx.rb
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
require 'base64'
|
2
|
+
require 'stringio'
|
3
|
+
require 'openssl'
|
4
|
+
require 'digest/sha2'
|
5
|
+
require 'fast-aes'
|
6
|
+
|
7
|
+
require 'keepassx/database'
|
8
|
+
require 'keepassx/entry'
|
9
|
+
require 'keepassx/entry_field'
|
10
|
+
require 'keepassx/group'
|
11
|
+
require 'keepassx/group_field'
|
12
|
+
require 'keepassx/header'
|
13
|
+
require 'keepassx/aes_crypt'
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Keepassx
|
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,45 @@
|
|
1
|
+
module Keepassx
|
2
|
+
class Database
|
3
|
+
|
4
|
+
attr_reader :header, :groups, :entries
|
5
|
+
|
6
|
+
def self.open(path)
|
7
|
+
content = File.respond_to?(:binread) ? File.binread(path) : File.read(path)
|
8
|
+
self.new(content)
|
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
|
+
backup = groups.detect { |g| g.name == "Backup" }
|
33
|
+
backup_group_id = backup && backup.group_id
|
34
|
+
entries.select { |e| e.group_id != backup_group_id && e.title =~ /#{pattern}/i }
|
35
|
+
end
|
36
|
+
|
37
|
+
def valid?
|
38
|
+
@header.valid?
|
39
|
+
end
|
40
|
+
|
41
|
+
def decrypt_payload
|
42
|
+
@payload = AESCrypt.decrypt(@encrypted_payload, @final_key, header.encryption_iv, 'AES-256-CBC')
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,81 @@
|
|
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 Keepassx
|
31
|
+
class Entry
|
32
|
+
def self.extract_from_payload(header, payload_io)
|
33
|
+
groups = []
|
34
|
+
header.nentries.times do
|
35
|
+
group = Entry.new(payload_io)
|
36
|
+
groups << group
|
37
|
+
end
|
38
|
+
groups
|
39
|
+
end
|
40
|
+
|
41
|
+
attr_reader :fields
|
42
|
+
|
43
|
+
def initialize(payload_io)
|
44
|
+
fields = []
|
45
|
+
begin
|
46
|
+
field = EntryField.new(payload_io)
|
47
|
+
fields << field
|
48
|
+
end while not field.terminator?
|
49
|
+
|
50
|
+
@fields = fields
|
51
|
+
end
|
52
|
+
|
53
|
+
def length
|
54
|
+
@fields.map(&:length).reduce(&:+)
|
55
|
+
end
|
56
|
+
|
57
|
+
def notes
|
58
|
+
@fields.detect { |field| field.name == 'notes' }.data.chomp("\000")
|
59
|
+
end
|
60
|
+
|
61
|
+
def password
|
62
|
+
@fields.detect { |field| field.name == 'password' }.data.chomp("\000")
|
63
|
+
end
|
64
|
+
|
65
|
+
def title
|
66
|
+
@fields.detect { |field| field.name == 'title' }.data.chomp("\000")
|
67
|
+
end
|
68
|
+
|
69
|
+
def username
|
70
|
+
@fields.detect { |field| field.name == 'username' }.data.chomp("\000")
|
71
|
+
end
|
72
|
+
|
73
|
+
def group_id
|
74
|
+
@fields.detect { |field| field.name == 'groupid' }.data
|
75
|
+
end
|
76
|
+
|
77
|
+
def inspect
|
78
|
+
"Entry<title=#{title.inspect}, username=[FILTERED], password=[FILTERED], notes=#{notes.inspect}>"
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
module Keepassx
|
2
|
+
class EntryField
|
3
|
+
FIELD_TYPES = [
|
4
|
+
[0x0, 'ignored', :null],
|
5
|
+
[0x1, 'uuid', :ascii],
|
6
|
+
[0x2, 'groupid', :int],
|
7
|
+
[0x3, 'imageid', :int],
|
8
|
+
[0x4, 'title', :string],
|
9
|
+
[0x5, 'url', :string],
|
10
|
+
[0x6, 'username', :string],
|
11
|
+
[0x7, 'password', :string],
|
12
|
+
[0x8, 'notes', :string],
|
13
|
+
[0x9, 'creation_time', :date],
|
14
|
+
[0xa, 'last_mod_time', :date],
|
15
|
+
[0xb, 'last_acc_time', :date],
|
16
|
+
[0xc, 'expiration_time', :date],
|
17
|
+
[0xd, 'binary_desc', :string],
|
18
|
+
[0xe, 'binary_data', :shunt],
|
19
|
+
[0xFFFF, 'terminator', :nil]
|
20
|
+
]
|
21
|
+
FIELD_TERMINATOR = 0xFFFF
|
22
|
+
TYPE_CODE_FIELD_SIZE = 2 # unsigned short integer
|
23
|
+
DATA_LENGTH_FIELD_SIZE = 4 # unsigned integer
|
24
|
+
|
25
|
+
|
26
|
+
attr_reader :name, :data_type, :data
|
27
|
+
|
28
|
+
def initialize(payload)
|
29
|
+
type_code, @data_length = payload.read(TYPE_CODE_FIELD_SIZE + DATA_LENGTH_FIELD_SIZE).unpack('SI')
|
30
|
+
@name, @data_type = _parse_type_code(type_code)
|
31
|
+
@data = payload.read(@data_length)
|
32
|
+
end
|
33
|
+
|
34
|
+
def terminator?
|
35
|
+
name == 'terminator'
|
36
|
+
end
|
37
|
+
|
38
|
+
def length
|
39
|
+
TYPE_CODE_FIELD_SIZE + DATA_LENGTH_FIELD_SIZE + @data_length
|
40
|
+
end
|
41
|
+
|
42
|
+
def _parse_type_code(type_code)
|
43
|
+
(_, name, data_type) = FIELD_TYPES.detect do |(code, *rest)|
|
44
|
+
code == type_code
|
45
|
+
end
|
46
|
+
[name, data_type]
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,56 @@
|
|
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 Keepassx
|
24
|
+
class Group
|
25
|
+
def self.extract_from_payload(header, payload_io)
|
26
|
+
groups = []
|
27
|
+
header.ngroups.times do
|
28
|
+
group = Group.new(payload_io)
|
29
|
+
groups << group
|
30
|
+
end
|
31
|
+
groups
|
32
|
+
end
|
33
|
+
|
34
|
+
def initialize(payload_io)
|
35
|
+
fields = []
|
36
|
+
begin
|
37
|
+
field = GroupField.new(payload_io)
|
38
|
+
fields << field
|
39
|
+
end while not field.terminator?
|
40
|
+
|
41
|
+
@fields = fields
|
42
|
+
end
|
43
|
+
|
44
|
+
def length
|
45
|
+
@fields.map(&:length).reduce(&:+)
|
46
|
+
end
|
47
|
+
|
48
|
+
def group_id
|
49
|
+
@fields.detect { |field| field.name == 'groupid' }.data
|
50
|
+
end
|
51
|
+
|
52
|
+
def name
|
53
|
+
@fields.detect { |field| field.name == 'group_name' }.data.chomp("\000")
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
module Keepassx
|
2
|
+
class GroupField
|
3
|
+
FIELD_TYPES = [
|
4
|
+
[0x0, 'ignored', :null],
|
5
|
+
[0x1, 'groupid', :int],
|
6
|
+
[0x2, 'group_name', :string],
|
7
|
+
[0x3, 'creation_time', :date],
|
8
|
+
[0x4, 'lastmod_time', :date],
|
9
|
+
[0x5, 'lastacc_time', :date],
|
10
|
+
[0x6, 'expire_time', :date],
|
11
|
+
[0x7, 'imageid', :int],
|
12
|
+
[0x8, 'level', :short],
|
13
|
+
[0x9, 'flags', :int],
|
14
|
+
[0xFFFF, 'terminator', :null]
|
15
|
+
]
|
16
|
+
FIELD_TERMINATOR = 0xFFFF
|
17
|
+
TYPE_CODE_FIELD_SIZE = 2 # unsigned short integer
|
18
|
+
DATA_LENGTH_FIELD_SIZE = 4 # unsigned integer
|
19
|
+
|
20
|
+
|
21
|
+
attr_reader :name, :data_type, :data
|
22
|
+
|
23
|
+
def initialize(payload)
|
24
|
+
type_code, @data_length = payload.read(TYPE_CODE_FIELD_SIZE + DATA_LENGTH_FIELD_SIZE).unpack('SI')
|
25
|
+
@name, @data_type = _parse_type_code(type_code)
|
26
|
+
@data = payload.read(@data_length)
|
27
|
+
end
|
28
|
+
|
29
|
+
def terminator?
|
30
|
+
name == 'terminator'
|
31
|
+
end
|
32
|
+
|
33
|
+
def length
|
34
|
+
TYPE_CODE_FIELD_SIZE + DATA_LENGTH_FIELD_SIZE + @data_length
|
35
|
+
end
|
36
|
+
|
37
|
+
def _parse_type_code(type_code)
|
38
|
+
(_, name, data_type) = FIELD_TYPES.detect do |(code, *rest)|
|
39
|
+
code == type_code
|
40
|
+
end
|
41
|
+
[name, data_type]
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,87 @@
|
|
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 Keepassx
|
36
|
+
class Header
|
37
|
+
|
38
|
+
ENCRYPTION_FLAGS = [
|
39
|
+
[1 , 'SHA2' ],
|
40
|
+
[2 , 'Rijndael'],
|
41
|
+
[2 , 'AES' ],
|
42
|
+
[4 , 'ArcFour' ],
|
43
|
+
[8 , 'TwoFish' ]
|
44
|
+
]
|
45
|
+
|
46
|
+
attr_reader :encryption_iv
|
47
|
+
attr_reader :ngroups, :nentries
|
48
|
+
|
49
|
+
def initialize(header_bytes)
|
50
|
+
@signature1 = header_bytes[0..4].unpack('L*').first
|
51
|
+
@signature2 = header_bytes[4..8].unpack('L*').first
|
52
|
+
@flags = header_bytes[8..12].unpack('L*').first
|
53
|
+
@version = header_bytes[12..16].unpack('L*').first
|
54
|
+
@master_seed = header_bytes[16...32]
|
55
|
+
@encryption_iv = header_bytes[32...48]
|
56
|
+
@ngroups = header_bytes[48..52].unpack('L*').first
|
57
|
+
@nentries = header_bytes[52..56].unpack('L*').first
|
58
|
+
@contents_hash = header_bytes[56..88]
|
59
|
+
@master_seed2 = header_bytes[88...120]
|
60
|
+
@rounds = header_bytes[120..-1].unpack('L*').first
|
61
|
+
end
|
62
|
+
|
63
|
+
def valid?
|
64
|
+
@signature1 == 0x9AA2D903 && @signature2 == 0xB54BFB65
|
65
|
+
end
|
66
|
+
|
67
|
+
def encryption_type
|
68
|
+
ENCRYPTION_FLAGS.each do |(flag_mask, encryption_type)|
|
69
|
+
return encryption_type if @flags & flag_mask
|
70
|
+
end
|
71
|
+
'Unknown'
|
72
|
+
end
|
73
|
+
|
74
|
+
def final_key(master_key)
|
75
|
+
key = Digest::SHA2.new.update(master_key).digest
|
76
|
+
aes = FastAES.new(@master_seed2)
|
77
|
+
|
78
|
+
@rounds.times do |i|
|
79
|
+
key = aes.encrypt(key)
|
80
|
+
end
|
81
|
+
|
82
|
+
key = Digest::SHA2.new.update(key).digest
|
83
|
+
key = Digest::SHA2.new.update(@master_seed + key).digest
|
84
|
+
key
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Keepassx::Database do
|
4
|
+
describe 'self.open' do
|
5
|
+
it "creates a new instance of the databse with the file" do
|
6
|
+
db = Keepassx::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 = Keepassx::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 = Keepassx::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 == ["Backup", "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
|
+
|
45
|
+
it "can search for entries case-insensitively" do
|
46
|
+
entries = @db.search "TEST"
|
47
|
+
entries.first.title.should == "test entry"
|
48
|
+
end
|
49
|
+
|
50
|
+
it "will find the current values of entries with history" do
|
51
|
+
entries = @db.search "entry2"
|
52
|
+
entries.size.should == 1
|
53
|
+
entries.first.title.should == "entry2"
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
data/spec/spec_helper.rb
ADDED
Binary file
|
metadata
ADDED
@@ -0,0 +1,108 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: keepassx
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease: false
|
5
|
+
segments:
|
6
|
+
- 0
|
7
|
+
- 1
|
8
|
+
- 0
|
9
|
+
version: 0.1.0
|
10
|
+
platform: ruby
|
11
|
+
authors:
|
12
|
+
- Tony Pitluga
|
13
|
+
- Paul Hinze
|
14
|
+
autorequire:
|
15
|
+
bindir: bin
|
16
|
+
cert_chain: []
|
17
|
+
|
18
|
+
date: 2012-10-14 00:00:00 -05:00
|
19
|
+
default_executable:
|
20
|
+
dependencies:
|
21
|
+
- !ruby/object:Gem::Dependency
|
22
|
+
name: fast-aes
|
23
|
+
prerelease: false
|
24
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
25
|
+
requirements:
|
26
|
+
- - ~>
|
27
|
+
- !ruby/object:Gem::Version
|
28
|
+
segments:
|
29
|
+
- 0
|
30
|
+
- 1
|
31
|
+
version: "0.1"
|
32
|
+
type: :runtime
|
33
|
+
version_requirements: *id001
|
34
|
+
- !ruby/object:Gem::Dependency
|
35
|
+
name: rspec
|
36
|
+
prerelease: false
|
37
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
38
|
+
requirements:
|
39
|
+
- - "="
|
40
|
+
- !ruby/object:Gem::Version
|
41
|
+
segments:
|
42
|
+
- 2
|
43
|
+
- 11
|
44
|
+
- 0
|
45
|
+
version: 2.11.0
|
46
|
+
type: :development
|
47
|
+
version_requirements: *id002
|
48
|
+
description: See http://github.com/pitluga/keepassx
|
49
|
+
email:
|
50
|
+
- tony.pitluga@gmail.com
|
51
|
+
- paul.t.hinze@gmail.com
|
52
|
+
executables: []
|
53
|
+
|
54
|
+
extensions: []
|
55
|
+
|
56
|
+
extra_rdoc_files: []
|
57
|
+
|
58
|
+
files:
|
59
|
+
- .rvmrc
|
60
|
+
- .travis.yml
|
61
|
+
- Gemfile
|
62
|
+
- Gemfile.lock
|
63
|
+
- README.md
|
64
|
+
- Rakefile
|
65
|
+
- keepassx.gemspec
|
66
|
+
- lib/keepassx.rb
|
67
|
+
- lib/keepassx/aes_crypt.rb
|
68
|
+
- lib/keepassx/database.rb
|
69
|
+
- lib/keepassx/entry.rb
|
70
|
+
- lib/keepassx/entry_field.rb
|
71
|
+
- lib/keepassx/group.rb
|
72
|
+
- lib/keepassx/group_field.rb
|
73
|
+
- lib/keepassx/header.rb
|
74
|
+
- spec/keepassx/database_spec.rb
|
75
|
+
- spec/spec_helper.rb
|
76
|
+
- spec/test_database.kdb
|
77
|
+
has_rdoc: true
|
78
|
+
homepage: http://github.com/pitluga/keepassx
|
79
|
+
licenses: []
|
80
|
+
|
81
|
+
post_install_message:
|
82
|
+
rdoc_options: []
|
83
|
+
|
84
|
+
require_paths:
|
85
|
+
- lib
|
86
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
87
|
+
requirements:
|
88
|
+
- - ">="
|
89
|
+
- !ruby/object:Gem::Version
|
90
|
+
segments:
|
91
|
+
- 0
|
92
|
+
version: "0"
|
93
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
94
|
+
requirements:
|
95
|
+
- - ">="
|
96
|
+
- !ruby/object:Gem::Version
|
97
|
+
segments:
|
98
|
+
- 0
|
99
|
+
version: "0"
|
100
|
+
requirements: []
|
101
|
+
|
102
|
+
rubyforge_project:
|
103
|
+
rubygems_version: 1.3.6
|
104
|
+
signing_key:
|
105
|
+
specification_version: 3
|
106
|
+
summary: Ruby API access for KeePassX databases
|
107
|
+
test_files: []
|
108
|
+
|