class Kdbx

Constants

VERSION

Attributes

content[RW]
header[RW]
keyfile[R]
password[R]

Public Class Methods

new(**options) click to toggle source
# File lib/kdbx.rb, line 16
def initialize(**options)
  @password = @keyfile = nil
  @header, @content = Header.new, String.new
  self.password = options[:password] if options.has_key? :password
  self.keyfile  = options[:keyfile]  if options.has_key? :keyfile
end
open(filename, **options) click to toggle source
# File lib/kdbx.rb, line 7
def self.open(filename, **options)
  new(**options).tap do |kdbx|
    File.open filename, "rb" do |file|
      kdbx.header = Header.load file
      kdbx.decrypt_content file.read
    end
  end
end

Public Instance Methods

compressionflags() click to toggle source
# File lib/kdbx/attributes.rb, line 40
def compressionflags
  @header[3].unpack("L").first
end
compressionflags=(flag) click to toggle source
# File lib/kdbx/attributes.rb, line 44
def compressionflags=(flag)
  @header[3] = [flag].pack("L")
end
credential() click to toggle source
# File lib/kdbx/attributes.rb, line 15
def credential
  cred = password || String.new
  return cred if keyfile == nil
  data = IO.read keyfile
  if !data.valid_encoding?
    return cred + sha256(data)
  end
  if data.bytesize == 32
    return cred + data
  end
  if data =~ /\A\h{64}\z/
    data = [data].pack("H*")
    return cred + data
  end
  begin
    xpath = "/KeyFile/Key/Data"
    tnd = REXML::Document.new(data).get_text(xpath)
    cred + Base64.decode64(tnd.to_s)
  rescue REXML::ParseException
    cred + sha256(data)
  end
end
decrypt_content(data) click to toggle source
# File lib/kdbx/crypto.rb, line 15
def decrypt_content(data)
  data = decrypt data.to_s
  if data.start_with? streamstartbytes
    size = streamstartbytes.bytesize
    data = data.byteslice size..-1
  else
    fail KeyError, "wrong password or keyfile"
  end
  data = decode data
  data = gunzip data if compressionflags == 1
  data = reverse data if innerrandomstreamid == 2
  @content = data
end
encrypt_content() click to toggle source
# File lib/kdbx/crypto.rb, line 8
def encrypt_content
  data = @content.to_s
  data = obfuscate data if innerrandomstreamid == 2
  data = gzip data if compressionflags == 1
  encrypt streamstartbytes + encode(data)
end
encryptioniv() click to toggle source
# File lib/kdbx/attributes.rb, line 64
def encryptioniv
  @header[7]
end
innerrandomstreamid() click to toggle source
# File lib/kdbx/attributes.rb, line 76
def innerrandomstreamid
  @header[10].unpack("L").first
end
innerrandomstreamid=(id) click to toggle source
# File lib/kdbx/attributes.rb, line 80
def innerrandomstreamid=(id)
  @header[10] = [id].pack("L")
end
inspect() click to toggle source
Calls superclass method
# File lib/kdbx/attributes.rb, line 86
def inspect
  super
end
keyfile=(str) click to toggle source
# File lib/kdbx/attributes.rb, line 11
def keyfile=(str)
  @keyfile = File.absolute_path str
end
masterseed() click to toggle source
# File lib/kdbx/attributes.rb, line 48
def masterseed
  @header[4]
end
password=(str) click to toggle source
# File lib/kdbx/attributes.rb, line 6
def password=(str)
  @password = str == nil ? "" : sha256(str)
end
protectedstreamkey() click to toggle source
# File lib/kdbx/attributes.rb, line 68
def protectedstreamkey
  @header[8]
end
save(filename) click to toggle source
# File lib/kdbx.rb, line 23
def save(filename)
  secure_write filename do |file|
    file.write header.dump
    file.write encrypt_content
  end
  true
end
streamstartbytes() click to toggle source
# File lib/kdbx/attributes.rb, line 72
def streamstartbytes
  @header[9]
end
transformrounds() click to toggle source
# File lib/kdbx/attributes.rb, line 56
def transformrounds
  @header[6].unpack("Q").first
end
transformrounds=(num) click to toggle source
# File lib/kdbx/attributes.rb, line 60
def transformrounds=(num)
  @header[6] = [num].pack("Q")
end
transformseed() click to toggle source
# File lib/kdbx/attributes.rb, line 52
def transformseed
  @header[5]
end

Private Instance Methods

decode(data) click to toggle source
# File lib/kdbx/crypto.rb, line 64
def decode(data)
  io = StringIO.new.binmode
  dt = StringIO.new data
  loop do
    t = dt.readpartial 40
    (hash, size) = t.unpack("x4a32L<")
    break io.string if size == 0
    block = dt.readpartial size
    if sha256(block) != hash
      fail "broken file"
    else
      io.write block
    end
  end
rescue TypeError, EOFError
  fail ParseError, "truncated payload"
end
decrypt(data) click to toggle source
# File lib/kdbx/crypto.rb, line 48
def decrypt(data)
  cipher = OpenSSL::Cipher.new("AES-256-CBC").decrypt
  cipher.iv, cipher.key = encryptioniv, masterkey
  cipher.update(data) + cipher.final
rescue OpenSSL::Cipher::CipherError
  fail KeyError, "wrong password or keyfile"
end
encode(data) click to toggle source
# File lib/kdbx/crypto.rb, line 56
def encode(data)
  StringIO.new.binmode.tap do |io|
    io.write "\x00" * 4 + sha256(data)
    io.write [data.bytesize].pack("L<")
    io.write data + "\x01" + "\x00" * 39
  end.string
end
encrypt(data) click to toggle source
# File lib/kdbx/crypto.rb, line 42
def encrypt(data)
  cipher = OpenSSL::Cipher.new("AES-256-CBC").encrypt
  cipher.iv, cipher.key = encryptioniv, masterkey
  cipher.update(data) + cipher.final
end
gunzip(data) click to toggle source
# File lib/kdbx/crypto.rb, line 89
def gunzip(data)
  StringIO.open data do |io|
    gz = Zlib::GzipReader.new io
    [gz.read, gz.close].first
  end
rescue Zlib::GzipFile::Error => e
  fail ParseError, e.message
end
gzip(data) click to toggle source
# File lib/kdbx/crypto.rb, line 82
def gzip(data)
  StringIO.open do |io|
    gz = Zlib::GzipWriter.new io.binmode
    gz.write data; gz.close; io.string
  end
end
masterkey() click to toggle source
# File lib/kdbx/crypto.rb, line 35
def masterkey
  cipher = OpenSSL::Cipher.new("AES-256-ECB").encrypt
  cipher.key, key = transformseed, sha256(credential)
  transformrounds.times { key = cipher.update key }
  sha256(masterseed + sha256(key))
end
nonce() click to toggle source
# File lib/kdbx/attributes.rb, line 92
def nonce
  "\xE8\x30\x09\x4B\x97\x20\x5D\x2A".b
end
obfuscate(data) click to toggle source
# File lib/kdbx/crypto.rb, line 105
def obfuscate(data)
  xpath = "//Value[@Protected='True']"
  doc, seq = REXML::Document.new(data), sequence
  doc.each_element xpath do |ele|
    t = ele.texts.join.bytes
    t.map! { |b| b ^ seq.next }
    t = Base64.encode64 t.pack("C*")
    ele.text = t.strip
  end
  doc.to_s
rescue REXML::ParseException => e
  fail FormatError, e.message
end
reverse(data) click to toggle source
# File lib/kdbx/crypto.rb, line 119
def reverse(data)
  xpath = "//Value[@Protected='True']"
  doc, seq = REXML::Document.new(data), sequence
  doc.each_element xpath do |ele|
    t = Base64.decode64(ele.texts.join).bytes
    ele.text = t.map! { |b| b ^ seq.next }.pack("C*")
  end
  doc.to_s
rescue REXML::ParseException => e
  fail ParseError, e.message
end
secure_write(name) { |file| ... } click to toggle source
# File lib/kdbx.rb, line 33
def secure_write(name)
  name  = File.absolute_path name
  index = -1 - File.extname(name).length
  temp  = 1.step do |i|
    t = name.dup.insert index, ".#{i}"
    break t unless File.exist? t
  end
  begin
    File.open(temp, "wb") { |file| yield file }
    File.rename temp, name
  ensure
    File.delete temp if File.exist? temp
  end
end
sequence() click to toggle source
# File lib/kdbx/crypto.rb, line 98
def sequence
  cipher = Salsa20.new sha256(protectedstreamkey), nonce
  Enumerator.new do |e|
    loop { cipher.encrypt("\x00" * 64).each_byte { |b| e << b } }
  end
end
sha256(data) click to toggle source
# File lib/kdbx/crypto.rb, line 31
def sha256(data)
  OpenSSL::Digest::SHA256.digest data
end