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 ADDED
@@ -0,0 +1 @@
1
+ rvm 1.8.7-p249@keepassx --create
@@ -0,0 +1,5 @@
1
+ language: ruby
2
+ rvm:
3
+ - 1.8.7
4
+ - 1.9.1
5
+ - 1.9.3
data/Gemfile ADDED
@@ -0,0 +1,5 @@
1
+ source "http://rubygems.org"
2
+
3
+ gemspec
4
+
5
+ gem 'rake', '0.8.7'
@@ -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)
File without changes
@@ -0,0 +1,5 @@
1
+ require 'rspec/core/rake_task'
2
+
3
+ task :default => :spec
4
+
5
+ RSpec::Core::RakeTask.new(:spec)
@@ -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
@@ -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
@@ -0,0 +1,5 @@
1
+ $:.unshift File.expand_path('../../lib', __FILE__)
2
+ require 'keepassx'
3
+ require 'rspec'
4
+
5
+ TEST_DATABASE_PATH = File.expand_path('../test_database.kdb', __FILE__)
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
+