class RubeePass
Attributes
attachment_decoder[R]
db[R]
protected_decryptor[R]
xml[R]
Public Class Methods
hilight?()
click to toggle source
# File lib/rubeepass.rb, line 100 def self.hilight? @@hilight ||= false return @@hilight end
new(kdbx, password, keyfile = nil, hilight = false)
click to toggle source
# File lib/rubeepass.rb, line 320 def initialize(kdbx, password, keyfile = nil, hilight = false) @@hilight = hilight @kdbx = Pathname.new(kdbx).expand_path @keyfile = nil @keyfile = Pathname.new(keyfile).expand_path if (keyfile) @password = password if (@kdbx.nil?) raise RubeePass::Error::FileNotFound.new("null") elsif (!@kdbx.exist?) raise RubeePass::Error::FileNotFound.new(@kdbx) elsif (!@kdbx.readable?) raise RubeePass::Error::FileNotReadable.new(@kdbx) end if (@keyfile) if (!@keyfile.exist?) raise RubeePass::Error::FileNotFound.new(@keyfile) elsif (!@keyfile.readable?) raise RubeePass::Error::FileNotReadable.new(@keyfile) end end end
Public Instance Methods
absolute_path(to, from = "/")
click to toggle source
# File lib/rubeepass.rb, line 59 def absolute_path(to, from = "/") return "/" if (to.nil? || to.empty? || (to == "/")) from = "/" if (to.start_with?("/")) path = Array.new from.split("/").each do |group| next if (group.empty?) case group when "." # Do nothing when ".." path.pop else path.push(group) end end to.split("/").each do |group| next if (group.empty?) case group when "." # Do nothing when ".." path.pop else path.push(group) end end return "/#{path.join("/")}" end
clear_clipboard(time = 0)
click to toggle source
# File lib/rubeepass.rb, line 92 def clear_clipboard(time = 0) @thread.kill if (@thread) @thread = Thread.new do sleep time copy_to_clipboard("", false) end end
copy_to_clipboard(string, err = true)
click to toggle source
# File lib/rubeepass.rb, line 105 def copy_to_clipboard(string, err = true) string = "" if (string.nil?) if (OS::Underlying.windows?) puts "Your OS is not currently supported!" if (err) return end return if (ENV["DISPLAY"].nil? || ENV["DISPLAY"].empty?) echo = ScoobyDoo.where_are_you("echo") if (OS.mac?) pbcopy = ScoobyDoo.where_are_you("pbcopy") rn = ScoobyDoo.where_are_you("reattach-to-user-namespace") cp = pbcopy if (ENV["TMUX"]) cp = nil cp = "#{rn} #{pbcopy}" if (rn) end if (cp) system("#{echo} -n #{string.shellescape} | #{cp}") else if (err) puts "Please install reattach-to-user-namespace!" end return end elsif (OS.posix?) xclip = ScoobyDoo.where_are_you("xclip") xsel = ScoobyDoo.where_are_you("xsel") ["clipboard", "primary", "secondary"].each do |sel| cp = nil if (xclip) # string = " \x7F" if (string.empty?) cp = "xclip -i -selection #{sel}" elsif (xsel) cp = "xsel -i --#{sel}" end if (cp) system("#{echo} -n #{string.shellescape} | #{cp}") else if (err) puts "Please install either xclip or xsel!" end return end end else puts "Your OS is not currently supported!" if (err) return end end
export(export_file, format)
click to toggle source
# File lib/rubeepass.rb, line 293 def export(export_file, format) start_opening File.open(export_file, "w") do |f| case format when "gzip" gz = Zlib::GzipWriter.new(f) gz.write(@xml) gz.close when "xml" f.write(@xml) end end end
find_group(path)
click to toggle source
# File lib/rubeepass.rb, line 308 def find_group(path) return @db.find_group(path) end
find_group_like(path)
click to toggle source
# File lib/rubeepass.rb, line 312 def find_group_like(path) return @db.find_group(path, true) end
fuzzy_find(input)
click to toggle source
# File lib/rubeepass.rb, line 316 def fuzzy_find(input) return @db.fuzzy_find(input) end
method_missing(method_name, *args)
click to toggle source
Calls superclass method
# File lib/rubeepass.rb, line 380 def method_missing(method_name, *args) if (method_name.to_s.match(/^clear_clipboard_after_/)) mn = method_name.to_s.gsub!(/^clear_clipboard_after_/, "") case mn when /^[0-9]+_sec(ond)?s$/ time = mn.gsub(/_sec(ond)?s$/, "").to_i clear_clipboard(time) when /^[0-9]+_min(ute)?s$/ time = mn.gsub(/_min(ute)?s$/, "").to_i clear_clipboard(time * 60) else super end else super end end
open()
click to toggle source
# File lib/rubeepass.rb, line 398 def open start_opening @protected_decryptor = ProtectedDecryptor.new( Digest::SHA256.digest( @header[Header::PROTECTED_STREAM_KEY] ), ["E830094B97205D2A"].pack("H*") ) parse_xml return self end
pwnedpasswords(group = @db)
click to toggle source
# File lib/rubeepass.rb, line 428 def pwnedpasswords(group = @db) return [] if (group.nil?) pwned = Array.new group.groups.each do |name, subgroup| pwned.concat(pwnedpasswords(subgroup)) end group.entries.each do |name, entry| pwned.push(entry) if (entry.is_pwned?) end return pwned end
to_s()
click to toggle source
# File lib/rubeepass.rb, line 562 def to_s return @db.to_s end
wait_to_exit()
click to toggle source
# File lib/rubeepass.rb, line 566 def wait_to_exit return if (@thread.nil?) begin @thread.join rescue Interrupt puts end end
Private Instance Methods
decompress(compressed)
click to toggle source
# File lib/rubeepass.rb, line 162 def decompress(compressed) if (!@header[Header::COMPRESSION]) # This feels like a hack m = compressed.read.match( /\<KeePassFile\>.+\<\/KeePassFile\>/m ) return m[0] if (m.length > 0) return nil end gzip = "" block_id = 0 loop do # Read block ID data = compressed.read(4) raise Error::InvalidGzip.new if (data.nil?) id = data.unpack("L*")[0] raise Error::InvalidGzip.new if (block_id != id) block_id += 1 # Read expected hash data = compressed.read(32) raise Error::InvalidGzip.new if (data.nil?) expected_hash = data # Read size data = compressed.read(4) raise Error::InvalidGzip.new if (data.nil?) size = data.unpack("L*")[0] # Break if size is 0 and expected hash is all 0's if (size == 0) expected_hash.each_byte do |byte| raise Error::InvalidGzip.new if (byte != 0) end break end # Read data and get actual hash data = compressed.read(size) actual_hash = Digest::SHA256.digest(data) # Check that actual hash is same as expected hash if (actual_hash != expected_hash) raise Error::InvalidGzip.new end # Append data gzip += data end # Unzip gzip data return Zlib::GzipReader.new(StringIO.new(gzip)).read end
derive_kdf3_key()
click to toggle source
# File lib/rubeepass.rb, line 220 def derive_kdf3_key case @version when Magic::VERSION4 raise Error::InvalidHeader.new("KDF3 with version 4") end irsi = "\x02\x00\x00\x00" if ( (@header[Header::MASTER_SEED].length != 32) || (@header[Header::TRANSFORM_SEED].length != 32) ) raise Error::InvalidHeader.new("Invalid seed size") elsif (@header[Header::INNER_RANDOM_STREAM_ID] != irsi) raise Error::NotSalsa.new end cipher = OpenSSL::Cipher::AES.new(256, :ECB) cipher.encrypt cipher.key = @header[Header::TRANSFORM_SEED] cipher.padding = 0 key = @initial_key @header[Header::TRANSFORM_ROUNDS].times do key = cipher.update(key) + cipher.final end transform_key = Digest::SHA256::digest(key) combined_key = @header[Header::MASTER_SEED] + transform_key @cipher = Cipher.new( @header[Header::CIPHER_ID], @header[Header::ENCRYPTION_IV], Digest::SHA256::digest(combined_key) ) end
derive_kdf4_key(file)
click to toggle source
# File lib/rubeepass.rb, line 257 def derive_kdf4_key(file) case @version when Magic::VERSION3, Magic::VERSION31 raise Error::InvalidHeader.new("KDF4 with version 3") end sha = file.read(32) hmac = file.read(32) if (sha.nil? || (sha.length != 32)) raise Error::InvalidHeader.new("Invalid SHA size") end if (hmac.nil? || (hmac.length != 32)) raise Error::InvalidHeader.new("Invalid HMAC size") end # TODO check SHA and HMAC (eh, later) # TODO implement kdf4 key derivation raise Error::NotSupported.new("AES with new KDF") end
derive_key(file)
click to toggle source
# File lib/rubeepass.rb, line 281 def derive_key(file) if ( @header[Header::TRANSFORM_ROUNDS].nil? || @header[Header::TRANSFORM_SEED].nil? ) derive_kdf4_key(file) else derive_kdf3_key end end
join_key_and_keyfile()
click to toggle source
# File lib/rubeepass.rb, line 344 def join_key_and_keyfile filehash = "" if (@keyfile) contents = File.readlines(@keyfile).join if (contents.length != contents.bytesize) contents = contents.unpack("H*").pack("H*") end if (contents[0..4] == "<?xml") # Parse XML for data doc = REXML::Document.new(contents) data = doc.elements["KeyFile/Key/Data"] raise Error::InvalidXML.new if (data.nil?) filehash = data.text.unpack("m*")[0] elsif (contents.length == 32) # Not XML but a 32 byte Key file filehash = contents elsif (contents.length == 64) # Not XML but a 64 byte Key file if (contents.match(/^[0-9A-Fa-f]+$/)) filehash = [contents].pack("H*") end else # Not a Key file filehash = Digest::SHA256.digest(contents) end end if @password.nil? @initial_key = Digest::SHA256.digest(filehash) else passhash = Digest::SHA256.digest(@password) @initial_key = Digest::SHA256.digest(passhash + filehash) end end
parse_xml()
click to toggle source
# File lib/rubeepass.rb, line 413 def parse_xml doc = REXML::Document.new(@xml) if (doc.elements["KeePassFile/Root"].nil?) raise Error::InvalidXML.new end @attachment_decoder = AttachmentDecoder.new( doc.elements["KeePassFile/Meta/Binaries"] ) root = doc.elements["KeePassFile/Root"] @db = Group.from_xml(self, nil, root) end
read_header(file)
click to toggle source
# File lib/rubeepass.rb, line 442 def read_header(file) header = Hash.new loop do data = file.read(1) break if (data.nil?) id = data.unpack("C*")[0] case @version when Magic::VERSION3, Magic::VERSION31 data = file.read(2) when Magic::VERSION4 data = file.read(4) else raise Error::InvalidHeader.new end raise Error::InvalidHeader.new if (data.nil?) size = data.unpack("S*")[0] data = file.read(size) if (data.nil? && (size > 0)) raise Error::InvalidHeader.new end case id when Header::CIPHER_ID header[id] = data.unpack("H*")[0] when Header::COMPRESSION header[id] = (data.unpack("L*")[0] > 0) when Header::END_OF_HEADER break when Header::KDF_PARAMETERS case @version when Magic::VERSION3, Magic::VERSION31 raise Error::InvalidHeader.new end # raise Error::NotSupported.new("Custom KDF params") when Header::PUBLIC_CUSTOM_DATA case @version when Magic::VERSION3, Magic::VERSION31 raise Error::InvalidHeader.new end raise Error::NotSupported.new("Public custom data") when Header::TRANSFORM_ROUNDS header[id] = data.unpack("Q*")[0] else case @version when Magic::VERSION4 case id when Header::INNER_RANDOM_STREAM_ID, Header::PROTECTED_STREAM_KEY, Header::STREAM_START_BYTES, Header::TRANSFORM_ROUNDS, Header::TRANSFORM_SEED raise Error::InvalidHeader.new( "Legacy header ID" ) end end header[id] = data end end @header = header end
read_magic_and_version(file)
click to toggle source
# File lib/rubeepass.rb, line 508 def read_magic_and_version(file) data = file.read(4) raise Error::InvalidMagic.new if (data.nil?) @sig1 = data.unpack("L*")[0] # raise Error::InvalidMagic.new if (@sig1 != Magic::SIG1) data = file.read(4) raise Error::InvalidMagic.new if (data.nil?) @sig2 = data.unpack("L*")[0] # raise Error::InvalidMagic.new if (@sig2 != Magic::SIG2) data = file.read(4) raise Error::InvalidVersion.new if (data.nil?) @version = data.unpack("L*")[0] case @version when Magic::VERSION3, Magic::VERSION31, Magic::VERSION4 else raise Error::InvalidVersion.new end end
start_opening()
click to toggle source
# File lib/rubeepass.rb, line 530 def start_opening @cipher = nil @db = nil @header = nil @initial_key = nil @sig1 = nil @sig2 = nil @version = nil @xml = nil file = File.open(@kdbx) # Read metadata and derive key read_magic_and_version(file) read_header(file) join_key_and_keyfile derive_key(file) # Decrypt file encrypted = file.read decrypted = @cipher.decrypt(encrypted) if (decrypted.read(32) != @header[Header::STREAM_START_BYTES]) raise Error::InvalidPassword.new end # Decompress (if necessary) @xml = decompress(decrypted) file.close end