keeper_secrets_manager 17.0.4 → 17.1.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.
Files changed (36) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +26 -15
  3. data/Gemfile +3 -3
  4. data/README.md +1 -1
  5. data/Rakefile +1 -1
  6. data/lib/keeper_secrets_manager/config_keys.rb +2 -2
  7. data/lib/keeper_secrets_manager/core.rb +594 -394
  8. data/lib/keeper_secrets_manager/crypto.rb +106 -113
  9. data/lib/keeper_secrets_manager/dto/payload.rb +4 -4
  10. data/lib/keeper_secrets_manager/dto.rb +50 -32
  11. data/lib/keeper_secrets_manager/errors.rb +13 -2
  12. data/lib/keeper_secrets_manager/field_types.rb +3 -3
  13. data/lib/keeper_secrets_manager/folder_manager.rb +25 -29
  14. data/lib/keeper_secrets_manager/keeper_globals.rb +9 -15
  15. data/lib/keeper_secrets_manager/notation.rb +99 -92
  16. data/lib/keeper_secrets_manager/notation_enhancements.rb +22 -24
  17. data/lib/keeper_secrets_manager/storage.rb +35 -36
  18. data/lib/keeper_secrets_manager/totp.rb +27 -27
  19. data/lib/keeper_secrets_manager/utils.rb +83 -17
  20. data/lib/keeper_secrets_manager/version.rb +2 -2
  21. data/lib/keeper_secrets_manager.rb +3 -3
  22. metadata +7 -21
  23. data/DEVELOPER_SETUP.md +0 -0
  24. data/MANUAL_TESTING_GUIDE.md +0 -332
  25. data/RUBY_SDK_COMPLETE_DOCUMENTATION.md +0 -354
  26. data/RUBY_SDK_COMPREHENSIVE_SUMMARY.md +0 -192
  27. data/examples/01_quick_start.rb +0 -45
  28. data/examples/02_authentication.rb +0 -82
  29. data/examples/03_retrieve_secrets.rb +0 -81
  30. data/examples/04_create_update_delete.rb +0 -104
  31. data/examples/05_field_types.rb +0 -135
  32. data/examples/06_files.rb +0 -137
  33. data/examples/07_folders.rb +0 -145
  34. data/examples/08_notation.rb +0 -103
  35. data/examples/09_totp.rb +0 -100
  36. data/examples/README.md +0 -89
@@ -8,7 +8,7 @@ module KeeperSecretsManager
8
8
  GCM_IV_LENGTH = 12
9
9
  GCM_TAG_LENGTH = 16
10
10
  AES_KEY_LENGTH = 32
11
-
11
+
12
12
  # Block size for padding
13
13
  BLOCK_SIZE = 16
14
14
 
@@ -50,38 +50,40 @@ module KeeperSecretsManager
50
50
  # Generate private key bytes
51
51
  private_key_bytes = generate_encryption_key_bytes
52
52
  private_key_str = bytes_to_url_safe_str(private_key_bytes)
53
-
53
+
54
54
  # Create EC key from private key bytes
55
55
  private_key_bn = OpenSSL::BN.new(private_key_bytes, 2)
56
-
56
+
57
57
  # OpenSSL 3.0 compatibility - use ASN1 sequence to create key
58
58
  group = OpenSSL::PKey::EC::Group.new('prime256v1')
59
-
59
+
60
60
  # Generate public key point
61
61
  public_key_point = group.generator.mul(private_key_bn)
62
-
62
+
63
63
  # Create ASN1 sequence for the key
64
64
  asn1 = OpenSSL::ASN1::Sequence([
65
- OpenSSL::ASN1::Integer(1),
66
- OpenSSL::ASN1::OctetString(private_key_bytes),
67
- OpenSSL::ASN1::ObjectId('prime256v1', 0, :EXPLICIT),
68
- OpenSSL::ASN1::BitString(public_key_point.to_octet_string(:uncompressed), 1, :EXPLICIT)
69
- ])
70
-
65
+ OpenSSL::ASN1::Integer(1),
66
+ OpenSSL::ASN1::OctetString(private_key_bytes),
67
+ OpenSSL::ASN1::ObjectId('prime256v1', 0, :EXPLICIT),
68
+ OpenSSL::ASN1::BitString(public_key_point.to_octet_string(:uncompressed), 1,
69
+ :EXPLICIT)
70
+ ])
71
+
71
72
  # Create key from DER
72
73
  key = OpenSSL::PKey::EC.new(asn1.to_der)
73
-
74
+
74
75
  # Get public key bytes (uncompressed format)
75
76
  public_key_bytes = key.public_key.to_octet_string(:uncompressed)
76
77
  public_key_str = bytes_to_url_safe_str(public_key_bytes)
77
-
78
+
78
79
  # Also store the EC key in DER format for compatibility
79
80
  private_key_der = key.to_der
80
-
81
+
81
82
  {
82
83
  private_key_str: private_key_str,
83
84
  public_key_str: public_key_str,
84
- private_key_bytes: private_key_der, # Store DER format
85
+ private_key_bytes: private_key_bytes, # Use raw 32 bytes
86
+ private_key_der: private_key_der, # Also provide DER format
85
87
  public_key_bytes: public_key_bytes,
86
88
  private_key_obj: key
87
89
  }
@@ -89,63 +91,59 @@ module KeeperSecretsManager
89
91
 
90
92
  # Encrypt with AES-GCM or fallback to CBC
91
93
  def encrypt_aes_gcm(data, key)
92
- begin
93
- cipher = OpenSSL::Cipher.new('AES-256-GCM')
94
- cipher.encrypt
95
-
96
- # Generate random IV
97
- iv = generate_random_bytes(GCM_IV_LENGTH)
98
- cipher.iv = iv
99
- cipher.key = key
100
-
101
- # Encrypt data
102
- encrypted = cipher.update(data) + cipher.final
103
-
104
- # Get authentication tag
105
- tag = cipher.auth_tag(GCM_TAG_LENGTH)
106
-
107
- # Combine IV + encrypted + tag
108
- iv + encrypted + tag
109
- rescue RuntimeError => e
110
- if e.message.include?('unsupported cipher')
111
- # Fallback to AES-CBC for older Ruby/OpenSSL
112
- encrypt_aes_cbc(data, key)
113
- else
114
- raise e
115
- end
94
+ cipher = OpenSSL::Cipher.new('AES-256-GCM')
95
+ cipher.encrypt
96
+
97
+ # Generate random IV
98
+ iv = generate_random_bytes(GCM_IV_LENGTH)
99
+ cipher.iv = iv
100
+ cipher.key = key
101
+
102
+ # Encrypt data
103
+ encrypted = cipher.update(data) + cipher.final
104
+
105
+ # Get authentication tag
106
+ tag = cipher.auth_tag(GCM_TAG_LENGTH)
107
+
108
+ # Combine IV + encrypted + tag
109
+ iv + encrypted + tag
110
+ rescue RuntimeError => e
111
+ if e.message.include?('unsupported cipher')
112
+ # Fallback to AES-CBC for older Ruby/OpenSSL
113
+ encrypt_aes_cbc(data, key)
114
+ else
115
+ raise e
116
116
  end
117
117
  end
118
118
 
119
119
  # Decrypt with AES-GCM or fallback to CBC
120
120
  def decrypt_aes_gcm(encrypted_data, key)
121
+ # Try GCM first
122
+ # Extract components
123
+ iv = encrypted_data[0...GCM_IV_LENGTH]
124
+ tag = encrypted_data[-GCM_TAG_LENGTH..]
125
+ ciphertext = encrypted_data[GCM_IV_LENGTH...-GCM_TAG_LENGTH]
126
+
127
+ cipher = OpenSSL::Cipher.new('AES-256-GCM')
128
+ cipher.decrypt
129
+ cipher.iv = iv
130
+ cipher.key = key
131
+ cipher.auth_tag = tag
132
+
133
+ cipher.update(ciphertext) + cipher.final
134
+ rescue RuntimeError => e
135
+ if e.message.include?('unsupported cipher')
136
+ # Fallback to AES-CBC
137
+ decrypt_aes_cbc(encrypted_data, key)
138
+ else
139
+ raise e
140
+ end
141
+ rescue OpenSSL::Cipher::CipherError => e
142
+ # Maybe it's CBC encrypted?
121
143
  begin
122
- # Try GCM first
123
- # Extract components
124
- iv = encrypted_data[0...GCM_IV_LENGTH]
125
- tag = encrypted_data[-GCM_TAG_LENGTH..]
126
- ciphertext = encrypted_data[GCM_IV_LENGTH...-GCM_TAG_LENGTH]
127
-
128
- cipher = OpenSSL::Cipher.new('AES-256-GCM')
129
- cipher.decrypt
130
- cipher.iv = iv
131
- cipher.key = key
132
- cipher.auth_tag = tag
133
-
134
- cipher.update(ciphertext) + cipher.final
135
- rescue RuntimeError => e
136
- if e.message.include?('unsupported cipher')
137
- # Fallback to AES-CBC
138
- decrypt_aes_cbc(encrypted_data, key)
139
- else
140
- raise e
141
- end
142
- rescue OpenSSL::Cipher::CipherError => e
143
- # Maybe it's CBC encrypted?
144
- begin
145
- decrypt_aes_cbc(encrypted_data, key)
146
- rescue
147
- raise DecryptionError, "Failed to decrypt data: #{e.message}"
148
- end
144
+ decrypt_aes_cbc(encrypted_data, key)
145
+ rescue StandardError
146
+ raise DecryptionError, "Failed to decrypt data: #{e.message}"
149
147
  end
150
148
  end
151
149
 
@@ -153,15 +151,14 @@ module KeeperSecretsManager
153
151
  def encrypt_aes_cbc(data, key, iv = nil)
154
152
  cipher = OpenSSL::Cipher.new('AES-256-CBC')
155
153
  cipher.encrypt
156
-
154
+
157
155
  iv ||= generate_random_bytes(BLOCK_SIZE)
158
156
  cipher.iv = iv
159
157
  cipher.key = key
160
-
161
- # Apply PKCS7 padding
162
- padded_data = pad_data(data)
163
- encrypted = cipher.update(padded_data) + cipher.final
164
-
158
+
159
+ # OpenSSL handles PKCS7 padding automatically in cipher.final
160
+ encrypted = cipher.update(data) + cipher.final
161
+
165
162
  # Return IV + encrypted
166
163
  iv + encrypted
167
164
  end
@@ -171,16 +168,16 @@ module KeeperSecretsManager
171
168
  # Extract IV
172
169
  iv = encrypted_data[0...BLOCK_SIZE]
173
170
  ciphertext = encrypted_data[BLOCK_SIZE..]
174
-
171
+
175
172
  cipher = OpenSSL::Cipher.new('AES-256-CBC')
176
173
  cipher.decrypt
177
174
  cipher.iv = iv
178
175
  cipher.key = key
179
-
176
+
177
+ # OpenSSL handles PKCS7 padding removal automatically in cipher.final
180
178
  decrypted = cipher.update(ciphertext) + cipher.final
181
-
182
- # Remove padding
183
- unpad_data(decrypted)
179
+
180
+ decrypted
184
181
  rescue OpenSSL::Cipher::CipherError => e
185
182
  raise DecryptionError, "Failed to decrypt data: #{e.message}"
186
183
  end
@@ -195,18 +192,16 @@ module KeeperSecretsManager
195
192
  # Remove PKCS7 padding
196
193
  def unpad_data(data)
197
194
  return data if data.empty?
198
-
195
+
199
196
  pad_len = data[-1].ord
200
-
197
+
201
198
  # Validate padding
202
199
  if pad_len > 0 && pad_len <= BLOCK_SIZE && pad_len <= data.length
203
200
  # Check if all padding bytes are the same
204
201
  padding = data[-pad_len..]
205
- if padding.bytes.all? { |b| b == pad_len }
206
- return data[0...-pad_len]
207
- end
202
+ return data[0...-pad_len] if padding.bytes.all? { |b| b == pad_len }
208
203
  end
209
-
204
+
210
205
  data
211
206
  end
212
207
 
@@ -214,21 +209,21 @@ module KeeperSecretsManager
214
209
  def generate_hmac(key, data)
215
210
  OpenSSL::HMAC.digest('SHA512', key, data)
216
211
  end
217
-
212
+
218
213
  # Generate ECDSA signature
219
214
  def sign_ec(data, private_key)
220
215
  # Use SHA256 for ECDSA signature
221
- digest = OpenSSL::Digest::SHA256.new
216
+ digest = OpenSSL::Digest.new('SHA256')
222
217
  private_key.sign(digest, data)
223
218
  end
224
219
 
225
220
  # Verify HMAC signature
226
221
  def verify_hmac(key, data, signature)
227
222
  expected = generate_hmac(key, data)
228
-
223
+
229
224
  # Constant time comparison
230
225
  return false unless expected.bytesize == signature.bytesize
231
-
226
+
232
227
  result = 0
233
228
  expected.bytes.zip(signature.bytes) { |a, b| result |= a ^ b }
234
229
  result == 0
@@ -237,14 +232,14 @@ module KeeperSecretsManager
237
232
  # Load private key from DER format
238
233
  def load_private_key_der(der_bytes, password = nil)
239
234
  OpenSSL::PKey.read(der_bytes, password)
240
- rescue => e
235
+ rescue StandardError => e
241
236
  raise CryptoError, "Failed to load private key: #{e.message}"
242
237
  end
243
238
 
244
239
  # Load public key from DER format
245
240
  def load_public_key_der(der_bytes)
246
241
  OpenSSL::PKey.read(der_bytes)
247
- rescue => e
242
+ rescue StandardError => e
248
243
  raise CryptoError, "Failed to load public key: #{e.message}"
249
244
  end
250
245
 
@@ -262,20 +257,20 @@ module KeeperSecretsManager
262
257
  def encrypt_ec(data, public_key_bytes)
263
258
  # Load public key
264
259
  public_key = load_ec_public_key(public_key_bytes)
265
-
260
+
266
261
  # Generate ephemeral key pair
267
262
  ephemeral = OpenSSL::PKey::EC.generate('prime256v1')
268
-
263
+
269
264
  # Perform ECDH to get shared secret
270
265
  # The shared secret is computed using ECDH between ephemeral private key and server public key
271
266
  shared_secret = ephemeral.dh_compute_key(public_key.public_key)
272
-
267
+
273
268
  # Derive encryption key using SHA256
274
269
  encryption_key = OpenSSL::Digest::SHA256.digest(shared_secret)
275
-
270
+
276
271
  # Encrypt data with AES-GCM
277
272
  encrypted_data = encrypt_aes_gcm(data, encryption_key)
278
-
273
+
279
274
  # Return ephemeral public key + encrypted data
280
275
  ephemeral_public = ephemeral.public_key.to_octet_string(:uncompressed)
281
276
  ephemeral_public + encrypted_data
@@ -286,17 +281,17 @@ module KeeperSecretsManager
286
281
  # Extract ephemeral public key (65 bytes for uncompressed)
287
282
  ephemeral_public_bytes = encrypted_data[0...65]
288
283
  ciphertext = encrypted_data[65..]
289
-
284
+
290
285
  # Create EC key with ephemeral public key
291
286
  group = OpenSSL::PKey::EC::Group.new('prime256v1')
292
287
  ephemeral_point = OpenSSL::PKey::EC::Point.new(group, ephemeral_public_bytes)
293
-
288
+
294
289
  # Compute shared secret using ECDH
295
290
  shared_secret = private_key.dh_compute_key(ephemeral_point)
296
-
291
+
297
292
  # Derive decryption key
298
293
  decryption_key = OpenSSL::Digest::SHA256.digest(shared_secret)
299
-
294
+
300
295
  # Decrypt data
301
296
  decrypt_aes_gcm(ciphertext, decryption_key)
302
297
  end
@@ -307,31 +302,29 @@ module KeeperSecretsManager
307
302
  def load_ec_public_key(public_key_bytes)
308
303
  # If the bytes are longer than 65, it might be DER encoded
309
304
  # Extract the raw point bytes (last 65 bytes)
310
- if public_key_bytes.bytesize > 65
311
- public_key_bytes = public_key_bytes[-65..-1]
312
- end
313
-
305
+ public_key_bytes = public_key_bytes[-65..-1] if public_key_bytes.bytesize > 65
306
+
314
307
  # For OpenSSL 3.0+, we need to create the key differently
315
308
  begin
316
309
  # Try the OpenSSL 3.0+ way first
317
310
  group = OpenSSL::PKey::EC::Group.new('prime256v1')
318
311
  point = OpenSSL::PKey::EC::Point.new(group, public_key_bytes)
319
-
312
+
320
313
  # Create key from point directly using ASN1
321
314
  asn1 = OpenSSL::ASN1::Sequence([
322
- OpenSSL::ASN1::Sequence([
323
- OpenSSL::ASN1::ObjectId("id-ecPublicKey"),
324
- OpenSSL::ASN1::ObjectId("prime256v1")
325
- ]),
326
- OpenSSL::ASN1::BitString(public_key_bytes)
327
- ])
328
-
315
+ OpenSSL::ASN1::Sequence([
316
+ OpenSSL::ASN1::ObjectId('id-ecPublicKey'),
317
+ OpenSSL::ASN1::ObjectId('prime256v1')
318
+ ]),
319
+ OpenSSL::ASN1::BitString(public_key_bytes)
320
+ ])
321
+
329
322
  OpenSSL::PKey::EC.new(asn1.to_der)
330
- rescue => e
323
+ rescue StandardError => e
331
324
  # Fall back to old method for older OpenSSL
332
325
  group = OpenSSL::PKey::EC::Group.new('prime256v1')
333
326
  point = OpenSSL::PKey::EC::Point.new(group, public_key_bytes)
334
-
327
+
335
328
  key = OpenSSL::PKey::EC.new(group)
336
329
  key.public_key = point
337
330
  key
@@ -345,4 +338,4 @@ module KeeperSecretsManager
345
338
  end
346
339
  end
347
340
  end
348
- end
341
+ end
@@ -20,7 +20,7 @@ module KeeperSecretsManager
20
20
  instance_variables.each do |var|
21
21
  key = var.to_s.delete('@')
22
22
  value = instance_variable_get(var)
23
-
23
+
24
24
  # Convert Ruby snake_case to camelCase for API
25
25
  api_key = Utils.snake_to_camel(key)
26
26
  hash[api_key] = value unless value.nil?
@@ -47,7 +47,7 @@ module KeeperSecretsManager
47
47
 
48
48
  # Create record payload
49
49
  class CreatePayload < BasePayload
50
- attr_accessor :record_uid, :record_key, :folder_uid, :folder_key,
50
+ attr_accessor :record_uid, :record_key, :folder_uid, :folder_key,
51
51
  :data, :sub_folder_uid
52
52
 
53
53
  def initialize
@@ -87,7 +87,7 @@ module KeeperSecretsManager
87
87
  # File upload payload
88
88
  class FileUploadPayload < BasePayload
89
89
  attr_accessor :file_record_uid, :file_record_key, :file_record_data,
90
- :owner_record_uid, :owner_record_data, :link_key, :file_size
90
+ :owner_record_uid, :owner_record_data, :owner_record_revision, :link_key, :file_size
91
91
 
92
92
  def initialize
93
93
  super()
@@ -149,4 +149,4 @@ module KeeperSecretsManager
149
149
  end
150
150
  end
151
151
  end
152
- end
152
+ end
@@ -7,6 +7,7 @@ module KeeperSecretsManager
7
7
  # Base class for dynamic record handling
8
8
  class KeeperRecord
9
9
  attr_accessor :uid, :title, :type, :fields, :custom, :notes, :folder_uid, :data, :revision, :files
10
+ attr_reader :record_key # Internal - stores decrypted record key (bytes) for file upload operations
10
11
 
11
12
  def initialize(attrs = {})
12
13
  if attrs.is_a?(Hash)
@@ -14,7 +15,7 @@ module KeeperSecretsManager
14
15
  @uid = attrs['recordUid'] || attrs['uid'] || attrs[:uid]
15
16
  @folder_uid = attrs['folderUid'] || attrs['folder_uid'] || attrs[:folder_uid]
16
17
  @revision = attrs['revision'] || attrs[:revision] || 0
17
-
18
+
18
19
  # Handle encrypted data or direct attributes
19
20
  if attrs['data']
20
21
  data = attrs['data'].is_a?(String) ? JSON.parse(attrs['data']) : attrs['data']
@@ -30,70 +31,87 @@ module KeeperSecretsManager
30
31
  @custom = attrs['custom'] || attrs[:custom] || []
31
32
  @notes = attrs['notes'] || attrs[:notes] || ''
32
33
  end
33
-
34
+
34
35
  @files = attrs['files'] || attrs[:files] || []
35
36
  @data = attrs
36
37
  end
37
-
38
+
38
39
  # Ensure fields are always arrays of hashes
39
40
  normalize_fields!
40
41
  end
41
42
 
42
43
  # Convert to hash for API submission
44
+ # This should match the structure of the decrypted 'data' field from server
45
+ # (does NOT include uid, revision, folder_uid - those are in the outer payload)
43
46
  def to_h
44
- {
45
- 'uid' => uid,
47
+ result = {
46
48
  'title' => title,
47
49
  'type' => type,
48
- 'fields' => fields,
49
- 'custom' => custom,
50
- 'notes' => notes,
51
- 'folder_uid' => folder_uid
52
- }.compact
50
+ 'fields' => fields
51
+ }
52
+
53
+ # Only include custom if it has entries (server doesn't send empty arrays)
54
+ result['custom'] = custom if custom && !custom.empty?
55
+
56
+ # Only include notes if present
57
+ result['notes'] = notes if notes && !notes.empty?
58
+
59
+ result
53
60
  end
54
61
 
55
- # Find field by type or label
56
- def get_field(type_or_label, custom_field = false)
57
- field_array = custom_field ? custom : fields
58
- field_array.find { |f| f['type'] == type_or_label || f['label'] == type_or_label }
62
+ # Find field by type or label (searches both fields and custom arrays)
63
+ def get_field(type_or_label)
64
+ # Search in fields first
65
+ field = fields.find { |f| f['type'] == type_or_label || f['label'] == type_or_label }
66
+ return field if field
67
+
68
+ # Search in custom fields
69
+ custom.find { |f| f['type'] == type_or_label || f['label'] == type_or_label }
59
70
  end
60
71
 
61
72
  # Get field value (always returns array)
62
- def get_field_value(type_or_label, custom_field = false)
63
- field = get_field(type_or_label, custom_field)
73
+ def get_field_value(type_or_label)
74
+ field = get_field(type_or_label)
64
75
  field ? field['value'] || [] : []
65
76
  end
66
77
 
67
78
  # Get single field value (first element)
68
- def get_field_value_single(type_or_label, custom_field = false)
69
- values = get_field_value(type_or_label, custom_field)
79
+ def get_field_value_single(type_or_label)
80
+ values = get_field_value(type_or_label)
70
81
  values.first
71
82
  end
72
83
 
73
84
  # Add or update field
74
- def set_field(type, value, label = nil, custom_field = false)
75
- field_array = custom_field ? @custom : @fields
76
-
85
+ def set_field(type, value, label = nil)
77
86
  # Ensure value is an array
78
87
  value = [value] unless value.is_a?(Array)
79
-
80
- # Find existing field
81
- existing = field_array.find { |f| f['type'] == type || (label && f['label'] == label) }
82
-
88
+
89
+ # Find existing field in both arrays
90
+ existing = @fields.find { |f| f['type'] == type || (label && f['label'] == label) }
91
+ existing ||= @custom.find { |f| f['type'] == type || (label && f['label'] == label) }
92
+
83
93
  if existing
84
94
  existing['value'] = value
85
95
  existing['label'] = label if label
86
96
  else
87
97
  new_field = { 'type' => type, 'value' => value }
88
98
  new_field['label'] = label if label
89
- field_array << new_field
99
+
100
+ # Decide which array to add to:
101
+ # - If it has a label, it's a custom field
102
+ # - If it's not a common field type, it's likely custom
103
+ if label || !common_field_types.include?(type)
104
+ @custom << new_field
105
+ else
106
+ @fields << new_field
107
+ end
90
108
  end
91
109
  end
92
110
 
93
111
  # Dynamic field access methods
94
112
  def method_missing(method, *args, &block)
95
113
  method_name = method.to_s
96
-
114
+
97
115
  # Handle setters
98
116
  if method_name.end_with?('=')
99
117
  field_name = method_name.chomp('=')
@@ -120,18 +138,18 @@ module KeeperSecretsManager
120
138
 
121
139
  def normalize_field_array(fields)
122
140
  return [] unless fields.is_a?(Array)
123
-
141
+
124
142
  fields.map do |field|
125
143
  next field if field.is_a?(Hash)
126
-
144
+
127
145
  # Convert to hash if needed
128
146
  field.to_h
129
147
  end
130
148
  end
131
149
 
132
150
  def common_field_types
133
- %w[login password url fileRef oneTimeCode name phone email address
134
- paymentCard bankAccount birthDate secureNote sshKey host
151
+ %w[login password url fileRef oneTimeCode name phone email address
152
+ paymentCard bankAccount birthDate secureNote sshKey host
135
153
  databaseType script passkey]
136
154
  end
137
155
  end
@@ -218,4 +236,4 @@ module KeeperSecretsManager
218
236
  end
219
237
  end
220
238
  end
221
- end
239
+ end
@@ -7,6 +7,7 @@ module KeeperSecretsManager
7
7
 
8
8
  # Authentication/authorization errors
9
9
  class AuthenticationError < Error; end
10
+
10
11
  class AccessDeniedError < AuthenticationError; end
11
12
 
12
13
  # API/network errors
@@ -22,7 +23,9 @@ module KeeperSecretsManager
22
23
 
23
24
  # Crypto errors
24
25
  class CryptoError < Error; end
26
+
25
27
  class DecryptionError < CryptoError; end
28
+
26
29
  class EncryptionError < CryptoError; end
27
30
 
28
31
  # Notation errors
@@ -30,7 +33,9 @@ module KeeperSecretsManager
30
33
 
31
34
  # Record errors
32
35
  class RecordError < Error; end
36
+
33
37
  class RecordNotFoundError < RecordError; end
38
+
34
39
  class RecordValidationError < RecordError; end
35
40
 
36
41
  # Server errors
@@ -46,13 +51,19 @@ module KeeperSecretsManager
46
51
 
47
52
  # Specific server error types
48
53
  class InvalidClientVersionError < ServerError; end
54
+
49
55
  class InvalidTokenError < ServerError; end
56
+
50
57
  class BadRequestError < ServerError; end
58
+
51
59
  class RecordUidNotFoundError < ServerError; end
60
+
52
61
  class FolderUidNotFoundError < ServerError; end
62
+
53
63
  class AccessViolationError < ServerError; end
64
+
54
65
  class ThrottledError < ServerError; end
55
-
66
+
56
67
  # Error factory
57
68
  class ErrorFactory
58
69
  def self.from_server_response(result_code, message = nil)
@@ -76,4 +87,4 @@ module KeeperSecretsManager
76
87
  end
77
88
  end
78
89
  end
79
- end
90
+ end
@@ -9,7 +9,7 @@ module KeeperSecretsManager
9
9
  @label = label
10
10
  @required = required
11
11
  @privacy_screen = privacy_screen
12
-
12
+
13
13
  # Ensure value is always an array
14
14
  @value = value.is_a?(Array) ? value : [value]
15
15
  end
@@ -103,7 +103,7 @@ module KeeperSecretsManager
103
103
  when String
104
104
  (Date.parse(date).to_time.to_f * 1000).to_i
105
105
  else
106
- raise ArgumentError, "Invalid date format"
106
+ raise ArgumentError, 'Invalid date format'
107
107
  end
108
108
  Field.new(type: 'birthDate', value: timestamp, label: label)
109
109
  end
@@ -149,4 +149,4 @@ module KeeperSecretsManager
149
149
  end
150
150
  end
151
151
  end
152
- end
152
+ end