keepassx 0.1.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.codeclimate.yml +30 -0
- data/.gitignore +9 -0
- data/.rubocop.yml +64 -0
- data/.travis.yml +12 -3
- data/Gemfile +4 -2
- data/Guardfile +16 -0
- data/LICENSE +19 -0
- data/README.md +33 -0
- data/Rakefile +3 -2
- data/keepassx.gemspec +20 -10
- data/lib/keepassx.rb +42 -3
- data/lib/keepassx/aes_crypt.rb +16 -6
- data/lib/keepassx/database.rb +218 -27
- data/lib/keepassx/database/dumper.rb +87 -0
- data/lib/keepassx/database/finder.rb +102 -0
- data/lib/keepassx/database/loader.rb +217 -0
- data/lib/keepassx/entry.rb +70 -38
- data/lib/keepassx/field/base.rb +191 -0
- data/lib/keepassx/field/entry.rb +32 -0
- data/lib/keepassx/field/group.rb +27 -0
- data/lib/keepassx/fieldable.rb +161 -0
- data/lib/keepassx/group.rb +93 -20
- data/lib/keepassx/hashable_payload.rb +6 -0
- data/lib/keepassx/header.rb +102 -27
- data/lib/keepassx/version.rb +5 -0
- data/spec/factories.rb +23 -0
- data/spec/fixtures/database_empty.kdb +0 -0
- data/spec/fixtures/database_test.kdb +0 -0
- data/spec/fixtures/database_test_dumped.yml +76 -0
- data/spec/fixtures/database_with_key.kdb +0 -0
- data/spec/fixtures/database_with_key.key +1 -0
- data/spec/fixtures/database_with_key2.key +1 -0
- data/spec/fixtures/test_data_array.yml +113 -0
- data/spec/fixtures/test_data_array_dumped.yml +124 -0
- data/spec/keepassx/database_spec.rb +491 -29
- data/spec/keepassx/entry_spec.rb +95 -0
- data/spec/keepassx/group_spec.rb +92 -0
- data/spec/keepassx_spec.rb +17 -0
- data/spec/spec_helper.rb +59 -3
- metadata +143 -69
- data/.rvmrc +0 -1
- data/Gemfile.lock +0 -28
- data/lib/keepassx/entry_field.rb +0 -49
- data/lib/keepassx/group_field.rb +0 -44
- data/spec/test_database.kdb +0 -0
| @@ -0,0 +1,191 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module Keepassx
         | 
| 4 | 
            +
              module Field
         | 
| 5 | 
            +
                class Base
         | 
| 6 | 
            +
             | 
| 7 | 
            +
                  FIELD_TERMINATOR       = 0xFFFF
         | 
| 8 | 
            +
                  TYPE_CODE_FIELD_SIZE   = 2 # unsigned short integer
         | 
| 9 | 
            +
                  DATA_LENGTH_FIELD_SIZE = 4 # unsigned integer
         | 
| 10 | 
            +
             | 
| 11 | 
            +
                  attr_reader :name, :data_type, :type_code
         | 
| 12 | 
            +
             | 
| 13 | 
            +
             | 
| 14 | 
            +
                  def initialize(payload)
         | 
| 15 | 
            +
                    if payload.is_a?(StringIO)
         | 
| 16 | 
            +
                      @type_code, data_length = payload.read(TYPE_CODE_FIELD_SIZE + DATA_LENGTH_FIELD_SIZE).unpack('SI')
         | 
| 17 | 
            +
                      _, @name, @data_type = self.class.fields_description.find { |type_code, _, _| type_code == @type_code }
         | 
| 18 | 
            +
             | 
| 19 | 
            +
                      # Not using setter because it should be raw data here
         | 
| 20 | 
            +
                      @data = payload.read(data_length)
         | 
| 21 | 
            +
             | 
| 22 | 
            +
                      # Set export_import_methods *after* setting data
         | 
| 23 | 
            +
                      set_export_import_methods(@data_type)
         | 
| 24 | 
            +
             | 
| 25 | 
            +
                    elsif payload.is_a?(Hash)
         | 
| 26 | 
            +
                      @name = payload[:name].to_s
         | 
| 27 | 
            +
                      @type_code, _, @data_type = self.class.fields_description.find { |_, name, _| name == @name }
         | 
| 28 | 
            +
             | 
| 29 | 
            +
                      # Set export_import_methods *before* setting data
         | 
| 30 | 
            +
                      set_export_import_methods(@data_type)
         | 
| 31 | 
            +
             | 
| 32 | 
            +
                      # Using setter because we need to convert data here
         | 
| 33 | 
            +
                      self.data = payload[:data]
         | 
| 34 | 
            +
                    end
         | 
| 35 | 
            +
                  end
         | 
| 36 | 
            +
             | 
| 37 | 
            +
             | 
| 38 | 
            +
                  def data
         | 
| 39 | 
            +
                    send(@export_method)
         | 
| 40 | 
            +
                  end
         | 
| 41 | 
            +
             | 
| 42 | 
            +
             | 
| 43 | 
            +
                  def data=(value)
         | 
| 44 | 
            +
                    send(@import_method, value)
         | 
| 45 | 
            +
                  end
         | 
| 46 | 
            +
             | 
| 47 | 
            +
             | 
| 48 | 
            +
                  def terminator?
         | 
| 49 | 
            +
                    name == 'terminator'
         | 
| 50 | 
            +
                  end
         | 
| 51 | 
            +
             | 
| 52 | 
            +
             | 
| 53 | 
            +
                  def length
         | 
| 54 | 
            +
                    TYPE_CODE_FIELD_SIZE + DATA_LENGTH_FIELD_SIZE + size
         | 
| 55 | 
            +
                  end
         | 
| 56 | 
            +
             | 
| 57 | 
            +
             | 
| 58 | 
            +
                  def size
         | 
| 59 | 
            +
                    case data_type
         | 
| 60 | 
            +
                    when :null
         | 
| 61 | 
            +
                      0
         | 
| 62 | 
            +
                    when :int
         | 
| 63 | 
            +
                      4
         | 
| 64 | 
            +
                    when :date
         | 
| 65 | 
            +
                      5
         | 
| 66 | 
            +
                    when :uuid
         | 
| 67 | 
            +
                      16
         | 
| 68 | 
            +
                    else
         | 
| 69 | 
            +
                      (@data.nil? && 0) || @data.length
         | 
| 70 | 
            +
                    end
         | 
| 71 | 
            +
                  end
         | 
| 72 | 
            +
             | 
| 73 | 
            +
             | 
| 74 | 
            +
                  def encode
         | 
| 75 | 
            +
                    buffer = [type_code, size].pack 'SI'
         | 
| 76 | 
            +
                    buffer << @data unless @data.nil?
         | 
| 77 | 
            +
                    buffer
         | 
| 78 | 
            +
                  end
         | 
| 79 | 
            +
             | 
| 80 | 
            +
             | 
| 81 | 
            +
                  private
         | 
| 82 | 
            +
             | 
| 83 | 
            +
             | 
| 84 | 
            +
                    # rubocop:disable Style/UnneededInterpolation
         | 
| 85 | 
            +
                    def set_export_import_methods(type)
         | 
| 86 | 
            +
                      @export_method = "#{type}".to_sym
         | 
| 87 | 
            +
                      @import_method = "#{type}=".to_sym
         | 
| 88 | 
            +
                    end
         | 
| 89 | 
            +
                    # rubocop:enable Style/UnneededInterpolation
         | 
| 90 | 
            +
             | 
| 91 | 
            +
             | 
| 92 | 
            +
                    ### EXPORT METHODS
         | 
| 93 | 
            +
             | 
| 94 | 
            +
                    def null
         | 
| 95 | 
            +
                      nil
         | 
| 96 | 
            +
                    end
         | 
| 97 | 
            +
             | 
| 98 | 
            +
             | 
| 99 | 
            +
                    def shunt
         | 
| 100 | 
            +
                      @data
         | 
| 101 | 
            +
                    end
         | 
| 102 | 
            +
             | 
| 103 | 
            +
             | 
| 104 | 
            +
                    def string
         | 
| 105 | 
            +
                      @data.chomp("\000")
         | 
| 106 | 
            +
                    end
         | 
| 107 | 
            +
             | 
| 108 | 
            +
             | 
| 109 | 
            +
                    def int
         | 
| 110 | 
            +
                      @data.unpack('I')[0]
         | 
| 111 | 
            +
                    end
         | 
| 112 | 
            +
             | 
| 113 | 
            +
             | 
| 114 | 
            +
                    def short
         | 
| 115 | 
            +
                      @data.unpack('S')[0]
         | 
| 116 | 
            +
                    end
         | 
| 117 | 
            +
             | 
| 118 | 
            +
             | 
| 119 | 
            +
                    def ascii
         | 
| 120 | 
            +
                      # TODO: Add spec
         | 
| 121 | 
            +
                      @data.unpack('H*')[0]
         | 
| 122 | 
            +
                    end
         | 
| 123 | 
            +
             | 
| 124 | 
            +
             | 
| 125 | 
            +
                    def date
         | 
| 126 | 
            +
                      buffer = @data.unpack('C5')
         | 
| 127 | 
            +
                      year   = (buffer[0] << 6) | (buffer[1] >> 2)
         | 
| 128 | 
            +
                      month  = ((buffer[1] & 0b11) << 2) | (buffer[2] >> 6)
         | 
| 129 | 
            +
                      day    = ((buffer[2] & 0b111111) >> 1)
         | 
| 130 | 
            +
                      hour   = ((buffer[2] & 0b1) << 4) | (buffer[3] >> 4)
         | 
| 131 | 
            +
                      min    = ((buffer[3] & 0b1111) << 2) | (buffer[4] >> 6)
         | 
| 132 | 
            +
                      sec    = ((buffer[4] & 0b111111))
         | 
| 133 | 
            +
             | 
| 134 | 
            +
                      Time.local(year, month, day, hour, min, sec)
         | 
| 135 | 
            +
                    end
         | 
| 136 | 
            +
             | 
| 137 | 
            +
             | 
| 138 | 
            +
                    ### IMPORT METHODS
         | 
| 139 | 
            +
             | 
| 140 | 
            +
                    # rubocop:disable Naming/UncommunicativeMethodParamName
         | 
| 141 | 
            +
                    def null=(_)
         | 
| 142 | 
            +
                      @data = nil
         | 
| 143 | 
            +
                    end
         | 
| 144 | 
            +
                    # rubocop:enable Naming/UncommunicativeMethodParamName
         | 
| 145 | 
            +
             | 
| 146 | 
            +
             | 
| 147 | 
            +
                    def shunt=(value)
         | 
| 148 | 
            +
                      @data = value
         | 
| 149 | 
            +
                    end
         | 
| 150 | 
            +
             | 
| 151 | 
            +
             | 
| 152 | 
            +
                    def string=(value)
         | 
| 153 | 
            +
                      @data = "#{value}\000"
         | 
| 154 | 
            +
                    end
         | 
| 155 | 
            +
             | 
| 156 | 
            +
             | 
| 157 | 
            +
                    def int=(value)
         | 
| 158 | 
            +
                      @data = [value].pack('I')
         | 
| 159 | 
            +
                    end
         | 
| 160 | 
            +
             | 
| 161 | 
            +
             | 
| 162 | 
            +
                    def short=(value)
         | 
| 163 | 
            +
                      @data = [value].pack('S')
         | 
| 164 | 
            +
                    end
         | 
| 165 | 
            +
             | 
| 166 | 
            +
             | 
| 167 | 
            +
                    def ascii=(value)
         | 
| 168 | 
            +
                      @data = [value].pack('H*')
         | 
| 169 | 
            +
                    end
         | 
| 170 | 
            +
             | 
| 171 | 
            +
             | 
| 172 | 
            +
                    def date=(value)
         | 
| 173 | 
            +
                      raise ArgumentError, "Expected: Time, String or Integer, got: '#{value.class}'." unless [Time, String, Integer].include?(value.class)
         | 
| 174 | 
            +
             | 
| 175 | 
            +
                      value = Time.parse(value) if value.is_a?(String)
         | 
| 176 | 
            +
                      value = Time.at(value) if value.is_a?(Integer)
         | 
| 177 | 
            +
             | 
| 178 | 
            +
                      sec, min, hour, day, month, year = value.to_a
         | 
| 179 | 
            +
             | 
| 180 | 
            +
                      @data = [
         | 
| 181 | 
            +
                        0x0000FFFF & ((year >> 6) & 0x0000003F),
         | 
| 182 | 
            +
                        0x0000FFFF & (((year & 0x0000003f) << 2) | ((month >> 2) & 0x00000003)),
         | 
| 183 | 
            +
                        0x0000FFFF & (((month & 0x00000003) << 6) | ((day & 0x0000001F) << 1) | ((hour >> 4) & 0x00000001)),
         | 
| 184 | 
            +
                        0x0000FFFF & (((hour & 0x0000000F) << 4) | ((min >> 2) & 0x0000000F)),
         | 
| 185 | 
            +
                        0x0000FFFF & (((min & 0x00000003) << 6) | (sec & 0x0000003F)),
         | 
| 186 | 
            +
                      ].pack('<C5')
         | 
| 187 | 
            +
                    end
         | 
| 188 | 
            +
             | 
| 189 | 
            +
                end
         | 
| 190 | 
            +
              end
         | 
| 191 | 
            +
            end
         | 
| @@ -0,0 +1,32 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module Keepassx
         | 
| 4 | 
            +
              module Field
         | 
| 5 | 
            +
                class Entry < Base
         | 
| 6 | 
            +
             | 
| 7 | 
            +
                  # rubocop:disable Metrics/MethodLength
         | 
| 8 | 
            +
                  def self.fields_description
         | 
| 9 | 
            +
                    @fields_description ||= [
         | 
| 10 | 
            +
                      [0x0, 'ignored',         :null],
         | 
| 11 | 
            +
                      [0x1, 'id',              :ascii],
         | 
| 12 | 
            +
                      [0x2, 'group_id',        :int],
         | 
| 13 | 
            +
                      [0x3, 'icon',            :int],
         | 
| 14 | 
            +
                      [0x4, 'name',            :string],
         | 
| 15 | 
            +
                      [0x5, 'url',             :string],
         | 
| 16 | 
            +
                      [0x6, 'username',        :string],
         | 
| 17 | 
            +
                      [0x7, 'password',        :string],
         | 
| 18 | 
            +
                      [0x8, 'notes',           :string],
         | 
| 19 | 
            +
                      [0x9, 'creation_time',   :date],
         | 
| 20 | 
            +
                      [0xa, 'last_mod_time',   :date],
         | 
| 21 | 
            +
                      [0xb, 'last_acc_time',   :date],
         | 
| 22 | 
            +
                      [0xc, 'expiration_time', :date],
         | 
| 23 | 
            +
                      [0xd, 'binary_desc',     :string],
         | 
| 24 | 
            +
                      [0xe, 'binary_data',     :shunt],
         | 
| 25 | 
            +
                      [0xFFFF, 'terminator',   :null],
         | 
| 26 | 
            +
                    ]
         | 
| 27 | 
            +
                  end
         | 
| 28 | 
            +
                  # rubocop:enable Metrics/MethodLength
         | 
| 29 | 
            +
             | 
| 30 | 
            +
                end
         | 
| 31 | 
            +
              end
         | 
| 32 | 
            +
            end
         | 
| @@ -0,0 +1,27 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module Keepassx
         | 
| 4 | 
            +
              module Field
         | 
| 5 | 
            +
                class Group < Base
         | 
| 6 | 
            +
             | 
| 7 | 
            +
                  # rubocop:disable Metrics/MethodLength
         | 
| 8 | 
            +
                  def self.fields_description
         | 
| 9 | 
            +
                    @fields_description ||= [
         | 
| 10 | 
            +
                      [0x0, 'ignored',         :null],
         | 
| 11 | 
            +
                      [0x1, 'id',              :int],
         | 
| 12 | 
            +
                      [0x2, 'name',            :string],
         | 
| 13 | 
            +
                      [0x3, 'creation_time',   :date],
         | 
| 14 | 
            +
                      [0x4, 'last_mod_time',   :date],
         | 
| 15 | 
            +
                      [0x5, 'last_acc_time',   :date],
         | 
| 16 | 
            +
                      [0x6, 'expiration_time', :date],
         | 
| 17 | 
            +
                      [0x7, 'icon',            :int],
         | 
| 18 | 
            +
                      [0x8, 'level',           :short],
         | 
| 19 | 
            +
                      [0x9, 'flags',           :int],
         | 
| 20 | 
            +
                      [0xFFFF, 'terminator',   :null],
         | 
| 21 | 
            +
                    ]
         | 
| 22 | 
            +
                  end
         | 
| 23 | 
            +
                  # rubocop:enable Metrics/MethodLength
         | 
| 24 | 
            +
             | 
| 25 | 
            +
                end
         | 
| 26 | 
            +
              end
         | 
| 27 | 
            +
            end
         | 
| @@ -0,0 +1,161 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module Keepassx
         | 
| 4 | 
            +
              class Fieldable
         | 
| 5 | 
            +
             | 
| 6 | 
            +
                attr_reader :fields
         | 
| 7 | 
            +
             | 
| 8 | 
            +
                def initialize(payload)
         | 
| 9 | 
            +
                  @fields = []
         | 
| 10 | 
            +
             | 
| 11 | 
            +
                  if payload.is_a?(StringIO)
         | 
| 12 | 
            +
                    @fields = decode_payload(payload)
         | 
| 13 | 
            +
                  elsif payload.is_a?(Hash)
         | 
| 14 | 
            +
                    yield
         | 
| 15 | 
            +
                  else
         | 
| 16 | 
            +
                    raise ArgumentError, "Expected StringIO or Hash, got #{payload.class}"
         | 
| 17 | 
            +
                  end
         | 
| 18 | 
            +
                end
         | 
| 19 | 
            +
             | 
| 20 | 
            +
             | 
| 21 | 
            +
                class << self
         | 
| 22 | 
            +
             | 
| 23 | 
            +
                  attr_reader :field_descriptor
         | 
| 24 | 
            +
             | 
| 25 | 
            +
                  def set_field_descriptor(klass)
         | 
| 26 | 
            +
                    @field_descriptor = klass
         | 
| 27 | 
            +
                    create_fieldable_methods(klass.fields_description)
         | 
| 28 | 
            +
                  end
         | 
| 29 | 
            +
             | 
| 30 | 
            +
             | 
| 31 | 
            +
                  def create_fieldable_methods(methods)
         | 
| 32 | 
            +
                    methods.each do |_, method, _|
         | 
| 33 | 
            +
                      define_method method do
         | 
| 34 | 
            +
                        get method
         | 
| 35 | 
            +
                      end
         | 
| 36 | 
            +
             | 
| 37 | 
            +
                      define_method "#{method}=" do |v|
         | 
| 38 | 
            +
                        set method, v
         | 
| 39 | 
            +
                      end
         | 
| 40 | 
            +
                    end
         | 
| 41 | 
            +
                  end
         | 
| 42 | 
            +
             | 
| 43 | 
            +
             | 
| 44 | 
            +
                  # Return the list of fields' names
         | 
| 45 | 
            +
                  def fields
         | 
| 46 | 
            +
                    @fields ||= field_descriptor.fields_description.map { |_, name, _| name }
         | 
| 47 | 
            +
                  end
         | 
| 48 | 
            +
             | 
| 49 | 
            +
                end
         | 
| 50 | 
            +
             | 
| 51 | 
            +
             | 
| 52 | 
            +
                def length
         | 
| 53 | 
            +
                  fields.map(&:length).reduce(&:+)
         | 
| 54 | 
            +
                end
         | 
| 55 | 
            +
             | 
| 56 | 
            +
             | 
| 57 | 
            +
                def to_hash(opts = {})
         | 
| 58 | 
            +
                  skip_date = opts.fetch(:skip_date, false)
         | 
| 59 | 
            +
             | 
| 60 | 
            +
                  result = {}
         | 
| 61 | 
            +
                  fields.each do |field|
         | 
| 62 | 
            +
                    next if excluded_field?(field.name)
         | 
| 63 | 
            +
                    next if date_field?(field.name) && skip_date
         | 
| 64 | 
            +
             | 
| 65 | 
            +
                    result[field.name] = field.data
         | 
| 66 | 
            +
                  end
         | 
| 67 | 
            +
                  result
         | 
| 68 | 
            +
                end
         | 
| 69 | 
            +
             | 
| 70 | 
            +
             | 
| 71 | 
            +
                def encode
         | 
| 72 | 
            +
                  buffer = +''
         | 
| 73 | 
            +
                  fields.each do |field|
         | 
| 74 | 
            +
                    buffer << field.encode
         | 
| 75 | 
            +
                  end
         | 
| 76 | 
            +
                  buffer
         | 
| 77 | 
            +
                end
         | 
| 78 | 
            +
             | 
| 79 | 
            +
             | 
| 80 | 
            +
                def inspect
         | 
| 81 | 
            +
                  output = []
         | 
| 82 | 
            +
                  default_fields.each_key do |field_name|
         | 
| 83 | 
            +
                    if field_name == :password
         | 
| 84 | 
            +
                      output << 'password=[FILTERED]'
         | 
| 85 | 
            +
                    else
         | 
| 86 | 
            +
                      output << "#{field_name}=#{send(field_name)}" unless field_name == :terminator
         | 
| 87 | 
            +
                    end
         | 
| 88 | 
            +
                  end
         | 
| 89 | 
            +
                  "<#{self.class} #{output.join(', ')}>"
         | 
| 90 | 
            +
                end
         | 
| 91 | 
            +
             | 
| 92 | 
            +
             | 
| 93 | 
            +
                private
         | 
| 94 | 
            +
             | 
| 95 | 
            +
             | 
| 96 | 
            +
                  def decode_payload(payload)
         | 
| 97 | 
            +
                    fields = []
         | 
| 98 | 
            +
             | 
| 99 | 
            +
                    loop do
         | 
| 100 | 
            +
                      field = self.class.field_descriptor.new(payload)
         | 
| 101 | 
            +
                      fields << field
         | 
| 102 | 
            +
                      break if field.terminator?
         | 
| 103 | 
            +
                    end
         | 
| 104 | 
            +
             | 
| 105 | 
            +
                    fields
         | 
| 106 | 
            +
                  end
         | 
| 107 | 
            +
             | 
| 108 | 
            +
             | 
| 109 | 
            +
                  def build_payload(payload)
         | 
| 110 | 
            +
                    fields = []
         | 
| 111 | 
            +
                    default_fields.merge(payload).each do |k, v|
         | 
| 112 | 
            +
                      fields << self.class.field_descriptor.new(name: k, data: v)
         | 
| 113 | 
            +
                    end
         | 
| 114 | 
            +
                    fields
         | 
| 115 | 
            +
                  end
         | 
| 116 | 
            +
             | 
| 117 | 
            +
             | 
| 118 | 
            +
                  def valid_integer?(field)
         | 
| 119 | 
            +
                    field.is_a?(Integer)
         | 
| 120 | 
            +
                  end
         | 
| 121 | 
            +
             | 
| 122 | 
            +
             | 
| 123 | 
            +
                  def valid_string?(field)
         | 
| 124 | 
            +
                    field.is_a?(String) && !field.empty?
         | 
| 125 | 
            +
                  end
         | 
| 126 | 
            +
             | 
| 127 | 
            +
             | 
| 128 | 
            +
                  def get(name)
         | 
| 129 | 
            +
                    field = @fields.find { |f| f.name == name.to_s }
         | 
| 130 | 
            +
                    field&.data
         | 
| 131 | 
            +
                  end
         | 
| 132 | 
            +
             | 
| 133 | 
            +
             | 
| 134 | 
            +
                  def set(name, value)
         | 
| 135 | 
            +
                    field = @fields.find { |f| f.name == name.to_s }
         | 
| 136 | 
            +
                    if field.nil?
         | 
| 137 | 
            +
                      field = self.class.field_descriptor.new(name: name, data: value)
         | 
| 138 | 
            +
                      @fields << field
         | 
| 139 | 
            +
                    else
         | 
| 140 | 
            +
                      field.data = value
         | 
| 141 | 
            +
                    end
         | 
| 142 | 
            +
                    field
         | 
| 143 | 
            +
                  end
         | 
| 144 | 
            +
             | 
| 145 | 
            +
             | 
| 146 | 
            +
                  def excluded_field?(field)
         | 
| 147 | 
            +
                    exclusion_list.include?(field)
         | 
| 148 | 
            +
                  end
         | 
| 149 | 
            +
             | 
| 150 | 
            +
             | 
| 151 | 
            +
                  def exclusion_list
         | 
| 152 | 
            +
                    %w[terminator]
         | 
| 153 | 
            +
                  end
         | 
| 154 | 
            +
             | 
| 155 | 
            +
             | 
| 156 | 
            +
                  def date_field?(field)
         | 
| 157 | 
            +
                    %w[creation_time last_mod_time last_acc_time expiration_time].include?(field)
         | 
| 158 | 
            +
                  end
         | 
| 159 | 
            +
             | 
| 160 | 
            +
              end
         | 
| 161 | 
            +
            end
         | 
    
        data/lib/keepassx/group.rb
    CHANGED
    
    | @@ -1,3 +1,5 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 1 3 | 
             
            # One group: [FIELDTYPE(FT)][FIELDSIZE(FS)][FIELDDATA(FD)]
         | 
| 2 4 | 
             
            #            [FT+FS+(FD)][FT+FS+(FD)][FT+FS+(FD)][FT+FS+(FD)][FT+FS+(FD)]...
         | 
| 3 5 | 
             
            #
         | 
| @@ -20,37 +22,108 @@ | |
| 20 22 | 
             
            #   * 0008: Level, FIELDSIZE = 2
         | 
| 21 23 | 
             
            #   * 0009: Flags, 32-bit value, FIELDSIZE = 4
         | 
| 22 24 | 
             
            #   * FFFF: Group entry terminator, FIELDSIZE must be 0
         | 
| 25 | 
            +
             | 
| 23 26 | 
             
            module Keepassx
         | 
| 24 | 
            -
              class Group
         | 
| 25 | 
            -
             | 
| 26 | 
            -
             | 
| 27 | 
            -
             | 
| 28 | 
            -
             | 
| 29 | 
            -
             | 
| 27 | 
            +
              class Group < Fieldable
         | 
| 28 | 
            +
             | 
| 29 | 
            +
                set_field_descriptor Keepassx::Field::Group
         | 
| 30 | 
            +
             | 
| 31 | 
            +
                attr_accessor :entries
         | 
| 32 | 
            +
                attr_reader   :parent
         | 
| 33 | 
            +
             | 
| 34 | 
            +
                def initialize(payload)
         | 
| 35 | 
            +
                  @parent  = nil
         | 
| 36 | 
            +
                  @entries = []
         | 
| 37 | 
            +
             | 
| 38 | 
            +
                  super do
         | 
| 39 | 
            +
                    # Do some validation
         | 
| 40 | 
            +
                    raise ArgumentError, "'id' is required (type: integer)" unless valid_integer?(payload[:id])
         | 
| 41 | 
            +
                    raise ArgumentError, "'name' is required (type: string)" unless valid_string?(payload[:name])
         | 
| 42 | 
            +
             | 
| 43 | 
            +
                    # First set @parent and @level.
         | 
| 44 | 
            +
                    # Remove key from payload to not interfere with KeePassX fields format
         | 
| 45 | 
            +
                    self.parent = payload.delete(:parent)
         | 
| 46 | 
            +
             | 
| 47 | 
            +
                    # Build list of fields
         | 
| 48 | 
            +
                    @fields = build_payload(payload)
         | 
| 30 49 | 
             
                  end
         | 
| 31 | 
            -
                  groups
         | 
| 32 50 | 
             
                end
         | 
| 33 51 |  | 
| 34 | 
            -
                def initialize(payload_io)
         | 
| 35 | 
            -
                  fields = []
         | 
| 36 | 
            -
                  begin
         | 
| 37 | 
            -
                    field = GroupField.new(payload_io)
         | 
| 38 | 
            -
                    fields << field
         | 
| 39 | 
            -
                  end while not field.terminator?
         | 
| 40 52 |  | 
| 41 | 
            -
             | 
| 53 | 
            +
                class << self
         | 
| 54 | 
            +
             | 
| 55 | 
            +
                  def extract_from_payload(header, payload)
         | 
| 56 | 
            +
                    groups = []
         | 
| 57 | 
            +
                    header.groups_count.times { groups << Group.new(payload) }
         | 
| 58 | 
            +
                    groups
         | 
| 59 | 
            +
                  end
         | 
| 60 | 
            +
             | 
| 42 61 | 
             
                end
         | 
| 43 62 |  | 
| 44 | 
            -
             | 
| 45 | 
            -
             | 
| 63 | 
            +
             | 
| 64 | 
            +
                def parent=(value)
         | 
| 65 | 
            +
                  raise ArgumentError, "Expected Keepassx::Group or nil, got #{value.class}" unless valid_parent?(value)
         | 
| 66 | 
            +
             | 
| 67 | 
            +
                  if value.is_a?(Keepassx::Group)
         | 
| 68 | 
            +
                    self.level = value.level + 1
         | 
| 69 | 
            +
                    @parent    = value
         | 
| 70 | 
            +
             | 
| 71 | 
            +
                  elsif value.nil?
         | 
| 72 | 
            +
                    # Assume, group is located at the top level, in case it has no parent
         | 
| 73 | 
            +
                    self.level = 0
         | 
| 74 | 
            +
                    @parent    = nil
         | 
| 75 | 
            +
                  end
         | 
| 46 76 | 
             
                end
         | 
| 47 77 |  | 
| 48 | 
            -
             | 
| 49 | 
            -
             | 
| 78 | 
            +
             | 
| 79 | 
            +
                # Redefine #level method to return 0 instead of nil
         | 
| 80 | 
            +
                def level
         | 
| 81 | 
            +
                  value = get :level
         | 
| 82 | 
            +
                  value.nil? ? 0 : value
         | 
| 50 83 | 
             
                end
         | 
| 51 84 |  | 
| 52 | 
            -
             | 
| 53 | 
            -
             | 
| 85 | 
            +
             | 
| 86 | 
            +
                def ==(other)
         | 
| 87 | 
            +
                  return false if other.nil?
         | 
| 88 | 
            +
             | 
| 89 | 
            +
                  parent == other.parent  &&
         | 
| 90 | 
            +
                    name   == other.name  &&
         | 
| 91 | 
            +
                    id     == other.id    &&
         | 
| 92 | 
            +
                    level  == other.level &&
         | 
| 93 | 
            +
                    icon   == other.icon
         | 
| 54 94 | 
             
                end
         | 
| 95 | 
            +
             | 
| 96 | 
            +
             | 
| 97 | 
            +
                private
         | 
| 98 | 
            +
             | 
| 99 | 
            +
             | 
| 100 | 
            +
                  # Redefine #level= to make it private :
         | 
| 101 | 
            +
                  # Setting group level only is a non-sense as it depends
         | 
| 102 | 
            +
                  # on parent group.
         | 
| 103 | 
            +
                  def level=(value)
         | 
| 104 | 
            +
                    set :level, value
         | 
| 105 | 
            +
                  end
         | 
| 106 | 
            +
             | 
| 107 | 
            +
             | 
| 108 | 
            +
                  def default_fields
         | 
| 109 | 
            +
                    @default_fields ||= {
         | 
| 110 | 
            +
                      id:              :unknown,
         | 
| 111 | 
            +
                      name:            :unknown,
         | 
| 112 | 
            +
                      creation_time:   Time.now,
         | 
| 113 | 
            +
                      last_mod_time:   Time.now,
         | 
| 114 | 
            +
                      last_acc_time:   Time.now,
         | 
| 115 | 
            +
                      expiration_time: Time.local(2999, 12, 28, 23, 59, 59),
         | 
| 116 | 
            +
                      icon:            1,
         | 
| 117 | 
            +
                      level:           0,
         | 
| 118 | 
            +
                      flags:           0,
         | 
| 119 | 
            +
                      terminator:      nil,
         | 
| 120 | 
            +
                    }
         | 
| 121 | 
            +
                  end
         | 
| 122 | 
            +
             | 
| 123 | 
            +
             | 
| 124 | 
            +
                  def valid_parent?(object)
         | 
| 125 | 
            +
                    object.is_a?(Keepassx::Group) || object.nil?
         | 
| 126 | 
            +
                  end
         | 
| 127 | 
            +
             | 
| 55 128 | 
             
              end
         | 
| 56 129 | 
             
            end
         |