Skip to content

Commit

Permalink
Extracted from setec_astronomy
Browse files Browse the repository at this point in the history
  • Loading branch information
pitluga committed Oct 14, 2012
0 parents commit 18455a8
Show file tree
Hide file tree
Showing 18 changed files with 513 additions and 0 deletions.
1 change: 1 addition & 0 deletions .rvmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
rvm 1.8.7-p249@keepassx --create
5 changes: 5 additions & 0 deletions .travis.yml
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
5 changes: 5 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
source "http://rubygems.org"

gemspec

gem 'rake', '0.8.7'
28 changes: 28 additions & 0 deletions Gemfile.lock
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 added README.md
Empty file.
5 changes: 5 additions & 0 deletions Rakefile
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)
14 changes: 14 additions & 0 deletions keepassx.gemspec
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
13 changes: 13 additions & 0 deletions lib/keepassx.rb
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'
19 changes: 19 additions & 0 deletions lib/keepassx/aes_crypt.rb
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
45 changes: 45 additions & 0 deletions lib/keepassx/database.rb
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
81 changes: 81 additions & 0 deletions lib/keepassx/entry.rb
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
49 changes: 49 additions & 0 deletions lib/keepassx/entry_field.rb
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
56 changes: 56 additions & 0 deletions lib/keepassx/group.rb
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
44 changes: 44 additions & 0 deletions lib/keepassx/group_field.rb
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
Loading

0 comments on commit 18455a8

Please sign in to comment.