lib/keepassx/database.rb in keepassx-0.1.0 vs lib/keepassx/database.rb in keepassx-1.0.0

- old
+ new

@@ -1,45 +1,236 @@ +# frozen_string_literal: true + module Keepassx class Database - attr_reader :header, :groups, :entries + include Database::Dumper + include Database::Loader + include Database::Finder - 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] + # Check database validity. + # + # @return [Boolean] + def valid? + header.valid? end - def entry(title) - @entries.detect { |e| e.title == title } + + # Get lock state + # + # @return [Boolean] + def locked? + @locked 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 + + # Add new group to database. + # + # @param opts [Hash] Options that will be passed to Keepassx::Group#new. + # @return [Keepassx::Group] + # rubocop:disable Metrics/MethodLength + def add_group(opts) + raise ArgumentError, "Expected Hash or Keepassx::Group, got #{opts.class}" unless valid_group?(opts) + + if opts.is_a?(Keepassx::Group) + # Assign parent group + parent = opts.parent + index = last_sibling_index(parent) + 1 + @groups.insert(index, opts) + + # Increment counter + header.groups_count += 1 + + # Return group + opts + + elsif opts.is_a?(Hash) + opts = deep_copy(opts) + opts = build_group_options(opts) + + # Create group + group = create_group(opts) + + # Increment counter + header.groups_count += 1 + + # Return group + group + end end + # rubocop:enable Metrics/MethodLength - 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 } + + # Add new entry to database. + # + # @param opts [Hash] Options that will be passed to Keepassx::Entry#new. + # @return [Keepassx::Entry] + def add_entry(opts) + raise ArgumentError, "Expected Hash or Keepassx::Entry, got #{opts.class}" unless valid_entry?(opts) + + if opts.is_a?(Keepassx::Entry) + # Add entry + @entries << opts + + # Increment counter + header.entries_count += 1 + + # Return entry + opts + + elsif opts.is_a?(Hash) + opts = deep_copy(opts) + opts = build_entry_options(opts) + + # Create entry + entry = create_entry(opts) + + # Increment counter + header.entries_count += 1 + + # Return entry + entry + end end - def valid? - @header.valid? + + def delete_group(item) + # Get group entries and delete them + group_entries = entries.select { |e| e.group == item } + group_entries.each { |e| delete_entry(e) } + + # Recursively delete ancestor groups + group_ancestors = groups.select { |g| g.parent == item } + group_ancestors.each { |g| delete_group(g) } + + item = groups.delete(item) + header.groups_count -= 1 + item end - def decrypt_payload - @payload = AESCrypt.decrypt(@encrypted_payload, @final_key, header.encryption_iv, 'AES-256-CBC') + + def delete_entry(item) + item = entries.delete(item) + header.entries_count -= 1 + item end + + + private + + + # Make deep copy of Hash + def deep_copy(opts) + Marshal.load Marshal.dump(opts) + end + + + # Get next group ID number. + # + # @return [Fixnum] + def next_group_id + if @groups.empty? + # Start each time from 1 to make sure groups get the same id's for the + # same input data + 1 + else + id = @groups.last.id + loop do + id += 1 + break id if @groups.find { |g| g.id == id }.nil? + end + end + end + + + # Retrieves last sibling index + # + # @param parent [Keepassx::Group] Last sibling group. + # @return [Integer] index Group index. + def last_sibling_index(parent) + return -1 if groups.empty? + + if parent.nil? + parent_index = 0 + sibling_level = 1 + else + parent_index = groups.find_index(parent) + sibling_level = parent.level + 1 + end + + raise "Could not find group #{parent.name}" if parent_index.nil? + + (parent_index..(header.groups_count - 1)).each do |i| + break i unless groups[i].level == sibling_level + end + end + + + def create_group(opts = {}) + group = Keepassx::Group.new(opts) + if group.parent.nil? + @groups << group + else + index = last_sibling_index(group.parent) + 1 + @groups.insert(index, group) + end + group + end + + + def build_group_options(opts = {}) + opts[:id] = next_group_id unless opts.key?(:id) + + # Replace parent, which is specified by symbol with actual group + if opts[:parent].is_a?(Symbol) + group = find_group(opts[:parent]) + raise "Group #{opts[:parent].inspect} does not exist" if group.nil? + + opts[:parent] = group + end + opts + end + + + def create_entry(opts = {}) + entry = Keepassx::Entry.new(opts) + @entries << entry + entry + end + + + # rubocop:disable Metrics/MethodLength, Style/SafeNavigation + def build_entry_options(opts = {}) + if opts[:group] + if opts[:group].is_a?(String) || opts[:group].is_a?(Hash) + group = find_group(opts[:group]) + raise "Group #{opts[:group].inspect} does not exist" if group.nil? + + opts[:group] = group + opts[:group_id] = group.id + elsif opts[:group].is_a?(Keepassx::Group) + opts[:group_id] = opts[:group].id + end + + elsif opts[:group_id] && opts[:group_id].is_a?(Integer) + group = find_group(id: opts[:group_id]) + raise "Group #{opts[:group_id].inspect} does not exist" if group.nil? + + opts[:group] = group + end + opts + end + # rubocop:enable Metrics/MethodLength, Style/SafeNavigation + + + def valid_group?(object) + object.is_a?(Keepassx::Group) || object.is_a?(Hash) + end + + + def valid_entry?(object) + object.is_a?(Keepassx::Entry) || object.is_a?(Hash) + end + end end