-
Notifications
You must be signed in to change notification settings - Fork 9
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit 18455a8
Showing
18 changed files
with
513 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
rvm 1.8.7-p249@keepassx --create |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
language: ruby | ||
rvm: | ||
- 1.8.7 | ||
- 1.9.1 | ||
- 1.9.3 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
source "http://rubygems.org" | ||
|
||
gemspec | ||
|
||
gem 'rake', '0.8.7' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
PATH | ||
remote: . | ||
specs: | ||
keepassx (0.1.0) | ||
fast-aes (~> 0.1) | ||
|
||
GEM | ||
remote: http://rubygems.org/ | ||
specs: | ||
diff-lcs (1.1.3) | ||
fast-aes (0.1.1) | ||
rake (0.8.7) | ||
rspec (2.11.0) | ||
rspec-core (~> 2.11.0) | ||
rspec-expectations (~> 2.11.0) | ||
rspec-mocks (~> 2.11.0) | ||
rspec-core (2.11.1) | ||
rspec-expectations (2.11.3) | ||
diff-lcs (~> 1.1.3) | ||
rspec-mocks (2.11.3) | ||
|
||
PLATFORMS | ||
ruby | ||
|
||
DEPENDENCIES | ||
keepassx! | ||
rake (= 0.8.7) | ||
rspec (= 2.11.0) |
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
require 'rspec/core/rake_task' | ||
|
||
task :default => :spec | ||
|
||
RSpec::Core::RakeTask.new(:spec) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
Gem::Specification.new do |s| | ||
s.name = "keepassx" | ||
s.summary = "Ruby API access for KeePassX databases" | ||
s.description = "See http://github.com/pitluga/keepassx" | ||
s.version = "0.1.0" | ||
s.authors = ["Tony Pitluga", "Paul Hinze"] | ||
s.email = ["[email protected]", "[email protected]"] | ||
s.homepage = "http://github.com/pitluga/keepassx" | ||
s.files = `git ls-files`.split("\n") | ||
|
||
s.add_dependency "fast-aes", "~> 0.1" | ||
|
||
s.add_development_dependency "rspec", "2.11.0" | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
require 'base64' | ||
require 'stringio' | ||
require 'openssl' | ||
require 'digest/sha2' | ||
require 'fast-aes' | ||
|
||
require 'keepassx/database' | ||
require 'keepassx/entry' | ||
require 'keepassx/entry_field' | ||
require 'keepassx/group' | ||
require 'keepassx/group_field' | ||
require 'keepassx/header' | ||
require 'keepassx/aes_crypt' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
module Keepassx | ||
module AESCrypt | ||
def self.decrypt(encrypted_data, key, iv, cipher_type) | ||
aes = OpenSSL::Cipher::Cipher.new(cipher_type) | ||
aes.decrypt | ||
aes.key = key | ||
aes.iv = iv unless iv.nil? | ||
aes.update(encrypted_data) + aes.final | ||
end | ||
|
||
def self.encrypt(data, key, iv, cipher_type) | ||
aes = OpenSSL::Cipher::Cipher.new(cipher_type) | ||
aes.encrypt | ||
aes.key = key | ||
aes.iv = iv unless iv.nil? | ||
aes.update(data) + aes.final | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
module Keepassx | ||
class Database | ||
|
||
attr_reader :header, :groups, :entries | ||
|
||
def self.open(path) | ||
content = File.respond_to?(:binread) ? File.binread(path) : File.read(path) | ||
self.new(content) | ||
end | ||
|
||
def initialize(raw_db) | ||
@header = Header.new(raw_db[0..124]) | ||
@encrypted_payload = raw_db[124..-1] | ||
end | ||
|
||
def entry(title) | ||
@entries.detect { |e| e.title == title } | ||
end | ||
|
||
def unlock(master_password) | ||
@final_key = header.final_key(master_password) | ||
decrypt_payload | ||
payload_io = StringIO.new(@payload) | ||
@groups = Group.extract_from_payload(header, payload_io) | ||
@entries = Entry.extract_from_payload(header, payload_io) | ||
true | ||
rescue OpenSSL::Cipher::CipherError | ||
false | ||
end | ||
|
||
def search(pattern) | ||
backup = groups.detect { |g| g.name == "Backup" } | ||
backup_group_id = backup && backup.group_id | ||
entries.select { |e| e.group_id != backup_group_id && e.title =~ /#{pattern}/i } | ||
end | ||
|
||
def valid? | ||
@header.valid? | ||
end | ||
|
||
def decrypt_payload | ||
@payload = AESCrypt.decrypt(@encrypted_payload, @final_key, header.encryption_iv, 'AES-256-CBC') | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,81 @@ | ||
# One entry: [FIELDTYPE(FT)][FIELDSIZE(FS)][FIELDDATA(FD)] | ||
# [FT+FS+(FD)][FT+FS+(FD)][FT+FS+(FD)][FT+FS+(FD)][FT+FS+(FD)]... | ||
|
||
# [ 2 bytes] FIELDTYPE | ||
# [ 4 bytes] FIELDSIZE, size of FIELDDATA in bytes | ||
# [ n bytes] FIELDDATA, n = FIELDSIZE | ||
|
||
# Notes: | ||
# - Strings are stored in UTF-8 encoded form and are null-terminated. | ||
# - FIELDTYPE can be one of the following identifiers: | ||
# * 0000: Invalid or comment block, block is ignored | ||
# * 0001: UUID, uniquely identifying an entry, FIELDSIZE must be 16 | ||
# * 0002: Group ID, identifying the group of the entry, FIELDSIZE = 4 | ||
# It can be any 32-bit value except 0 and 0xFFFFFFFF | ||
# * 0003: Image ID, identifying the image/icon of the entry, FIELDSIZE = 4 | ||
# * 0004: Title of the entry, FIELDDATA is an UTF-8 encoded string | ||
# * 0005: URL string, FIELDDATA is an UTF-8 encoded string | ||
# * 0006: UserName string, FIELDDATA is an UTF-8 encoded string | ||
# * 0007: Password string, FIELDDATA is an UTF-8 encoded string | ||
# * 0008: Notes string, FIELDDATA is an UTF-8 encoded string | ||
# * 0009: Creation time, FIELDSIZE = 5, FIELDDATA = packed date/time | ||
# * 000A: Last modification time, FIELDSIZE = 5, FIELDDATA = packed date/time | ||
# * 000B: Last access time, FIELDSIZE = 5, FIELDDATA = packed date/time | ||
# * 000C: Expiration time, FIELDSIZE = 5, FIELDDATA = packed date/time | ||
# * 000D: Binary description UTF-8 encoded string | ||
# * 000E: Binary data | ||
# * FFFF: Entry terminator, FIELDSIZE must be 0 | ||
# ''' | ||
|
||
module Keepassx | ||
class Entry | ||
def self.extract_from_payload(header, payload_io) | ||
groups = [] | ||
header.nentries.times do | ||
group = Entry.new(payload_io) | ||
groups << group | ||
end | ||
groups | ||
end | ||
|
||
attr_reader :fields | ||
|
||
def initialize(payload_io) | ||
fields = [] | ||
begin | ||
field = EntryField.new(payload_io) | ||
fields << field | ||
end while not field.terminator? | ||
|
||
@fields = fields | ||
end | ||
|
||
def length | ||
@fields.map(&:length).reduce(&:+) | ||
end | ||
|
||
def notes | ||
@fields.detect { |field| field.name == 'notes' }.data.chomp("\000") | ||
end | ||
|
||
def password | ||
@fields.detect { |field| field.name == 'password' }.data.chomp("\000") | ||
end | ||
|
||
def title | ||
@fields.detect { |field| field.name == 'title' }.data.chomp("\000") | ||
end | ||
|
||
def username | ||
@fields.detect { |field| field.name == 'username' }.data.chomp("\000") | ||
end | ||
|
||
def group_id | ||
@fields.detect { |field| field.name == 'groupid' }.data | ||
end | ||
|
||
def inspect | ||
"Entry<title=#{title.inspect}, username=[FILTERED], password=[FILTERED], notes=#{notes.inspect}>" | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
module Keepassx | ||
class EntryField | ||
FIELD_TYPES = [ | ||
[0x0, 'ignored', :null], | ||
[0x1, 'uuid', :ascii], | ||
[0x2, 'groupid', :int], | ||
[0x3, 'imageid', :int], | ||
[0x4, 'title', :string], | ||
[0x5, 'url', :string], | ||
[0x6, 'username', :string], | ||
[0x7, 'password', :string], | ||
[0x8, 'notes', :string], | ||
[0x9, 'creation_time', :date], | ||
[0xa, 'last_mod_time', :date], | ||
[0xb, 'last_acc_time', :date], | ||
[0xc, 'expiration_time', :date], | ||
[0xd, 'binary_desc', :string], | ||
[0xe, 'binary_data', :shunt], | ||
[0xFFFF, 'terminator', :nil] | ||
] | ||
FIELD_TERMINATOR = 0xFFFF | ||
TYPE_CODE_FIELD_SIZE = 2 # unsigned short integer | ||
DATA_LENGTH_FIELD_SIZE = 4 # unsigned integer | ||
|
||
|
||
attr_reader :name, :data_type, :data | ||
|
||
def initialize(payload) | ||
type_code, @data_length = payload.read(TYPE_CODE_FIELD_SIZE + DATA_LENGTH_FIELD_SIZE).unpack('SI') | ||
@name, @data_type = _parse_type_code(type_code) | ||
@data = payload.read(@data_length) | ||
end | ||
|
||
def terminator? | ||
name == 'terminator' | ||
end | ||
|
||
def length | ||
TYPE_CODE_FIELD_SIZE + DATA_LENGTH_FIELD_SIZE + @data_length | ||
end | ||
|
||
def _parse_type_code(type_code) | ||
(_, name, data_type) = FIELD_TYPES.detect do |(code, *rest)| | ||
code == type_code | ||
end | ||
[name, data_type] | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
# One group: [FIELDTYPE(FT)][FIELDSIZE(FS)][FIELDDATA(FD)] | ||
# [FT+FS+(FD)][FT+FS+(FD)][FT+FS+(FD)][FT+FS+(FD)][FT+FS+(FD)]... | ||
# | ||
# [ 2 bytes] FIELDTYPE | ||
# [ 4 bytes] FIELDSIZE, size of FIELDDATA in bytes | ||
# [ n bytes] FIELDDATA, n = FIELDSIZE | ||
# | ||
# Notes: | ||
# - Strings are stored in UTF-8 encoded form and are null-terminated. | ||
# - FIELDTYPE can be one of the following identifiers: | ||
# * 0000: Invalid or comment block, block is ignored | ||
# * 0001: Group ID, FIELDSIZE must be 4 bytes | ||
# It can be any 32-bit value except 0 and 0xFFFFFFFF | ||
# * 0002: Group name, FIELDDATA is an UTF-8 encoded string | ||
# * 0003: Creation time, FIELDSIZE = 5, FIELDDATA = packed date/time | ||
# * 0004: Last modification time, FIELDSIZE = 5, FIELDDATA = packed date/time | ||
# * 0005: Last access time, FIELDSIZE = 5, FIELDDATA = packed date/time | ||
# * 0006: Expiration time, FIELDSIZE = 5, FIELDDATA = packed date/time | ||
# * 0007: Image ID, FIELDSIZE must be 4 bytes | ||
# * 0008: Level, FIELDSIZE = 2 | ||
# * 0009: Flags, 32-bit value, FIELDSIZE = 4 | ||
# * FFFF: Group entry terminator, FIELDSIZE must be 0 | ||
module Keepassx | ||
class Group | ||
def self.extract_from_payload(header, payload_io) | ||
groups = [] | ||
header.ngroups.times do | ||
group = Group.new(payload_io) | ||
groups << group | ||
end | ||
groups | ||
end | ||
|
||
def initialize(payload_io) | ||
fields = [] | ||
begin | ||
field = GroupField.new(payload_io) | ||
fields << field | ||
end while not field.terminator? | ||
|
||
@fields = fields | ||
end | ||
|
||
def length | ||
@fields.map(&:length).reduce(&:+) | ||
end | ||
|
||
def group_id | ||
@fields.detect { |field| field.name == 'groupid' }.data | ||
end | ||
|
||
def name | ||
@fields.detect { |field| field.name == 'group_name' }.data.chomp("\000") | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
module Keepassx | ||
class GroupField | ||
FIELD_TYPES = [ | ||
[0x0, 'ignored', :null], | ||
[0x1, 'groupid', :int], | ||
[0x2, 'group_name', :string], | ||
[0x3, 'creation_time', :date], | ||
[0x4, 'lastmod_time', :date], | ||
[0x5, 'lastacc_time', :date], | ||
[0x6, 'expire_time', :date], | ||
[0x7, 'imageid', :int], | ||
[0x8, 'level', :short], | ||
[0x9, 'flags', :int], | ||
[0xFFFF, 'terminator', :null] | ||
] | ||
FIELD_TERMINATOR = 0xFFFF | ||
TYPE_CODE_FIELD_SIZE = 2 # unsigned short integer | ||
DATA_LENGTH_FIELD_SIZE = 4 # unsigned integer | ||
|
||
|
||
attr_reader :name, :data_type, :data | ||
|
||
def initialize(payload) | ||
type_code, @data_length = payload.read(TYPE_CODE_FIELD_SIZE + DATA_LENGTH_FIELD_SIZE).unpack('SI') | ||
@name, @data_type = _parse_type_code(type_code) | ||
@data = payload.read(@data_length) | ||
end | ||
|
||
def terminator? | ||
name == 'terminator' | ||
end | ||
|
||
def length | ||
TYPE_CODE_FIELD_SIZE + DATA_LENGTH_FIELD_SIZE + @data_length | ||
end | ||
|
||
def _parse_type_code(type_code) | ||
(_, name, data_type) = FIELD_TYPES.detect do |(code, *rest)| | ||
code == type_code | ||
end | ||
[name, data_type] | ||
end | ||
end | ||
end |
Oops, something went wrong.