keeper_secrets_manager 17.0.4 → 17.2.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 (39) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +100 -16
  3. data/Gemfile +6 -3
  4. data/README.md +75 -270
  5. data/Rakefile +1 -1
  6. data/bin/console +47 -0
  7. data/keeper_secrets_manager.gemspec +36 -0
  8. data/lib/keeper_secrets_manager/cache.rb +139 -0
  9. data/lib/keeper_secrets_manager/config_keys.rb +4 -2
  10. data/lib/keeper_secrets_manager/core.rb +993 -452
  11. data/lib/keeper_secrets_manager/crypto.rb +101 -116
  12. data/lib/keeper_secrets_manager/dto/payload.rb +7 -6
  13. data/lib/keeper_secrets_manager/dto.rb +374 -38
  14. data/lib/keeper_secrets_manager/errors.rb +13 -2
  15. data/lib/keeper_secrets_manager/field_types.rb +3 -3
  16. data/lib/keeper_secrets_manager/folder_manager.rb +25 -29
  17. data/lib/keeper_secrets_manager/keeper_globals.rb +11 -17
  18. data/lib/keeper_secrets_manager/notation.rb +202 -93
  19. data/lib/keeper_secrets_manager/notation_enhancements.rb +24 -24
  20. data/lib/keeper_secrets_manager/storage.rb +38 -38
  21. data/lib/keeper_secrets_manager/totp.rb +27 -27
  22. data/lib/keeper_secrets_manager/utils.rb +85 -18
  23. data/lib/keeper_secrets_manager/version.rb +2 -2
  24. data/lib/keeper_secrets_manager.rb +11 -3
  25. metadata +39 -22
  26. data/DEVELOPER_SETUP.md +0 -0
  27. data/MANUAL_TESTING_GUIDE.md +0 -332
  28. data/RUBY_SDK_COMPLETE_DOCUMENTATION.md +0 -354
  29. data/RUBY_SDK_COMPREHENSIVE_SUMMARY.md +0 -192
  30. data/examples/01_quick_start.rb +0 -45
  31. data/examples/02_authentication.rb +0 -82
  32. data/examples/03_retrieve_secrets.rb +0 -81
  33. data/examples/04_create_update_delete.rb +0 -104
  34. data/examples/05_field_types.rb +0 -135
  35. data/examples/06_files.rb +0 -137
  36. data/examples/07_folders.rb +0 -145
  37. data/examples/08_notation.rb +0 -103
  38. data/examples/09_totp.rb +0 -100
  39. data/examples/README.md +0 -89
@@ -6,62 +6,62 @@ module KeeperSecretsManager
6
6
  # Get value with enhanced functionality
7
7
  # This method extends the basic parse method to handle special cases
8
8
  def get_value(notation, options = {})
9
+ return nil if notation.nil? || !notation.is_a?(String) || notation.empty?
10
+
9
11
  value = parse(notation)
10
-
12
+
11
13
  # Check if we should process special types
12
14
  return value unless options[:auto_process]
13
-
15
+
14
16
  # Parse the notation to understand what we're dealing with
15
17
  parsed = parse_notation(notation)
16
18
  return value if parsed.length < 3
17
-
19
+
18
20
  selector = parsed[2].text&.first
19
21
  return value unless selector
20
-
22
+
21
23
  case selector.downcase
22
24
  when 'file'
23
25
  # If it's a file and auto_download is enabled, download it
24
26
  if options[:auto_download] && value.is_a?(Hash) && value['fileUid']
25
27
  begin
26
28
  file_data = @secrets_manager.download_file(value['fileUid'])
27
- return file_data['data'] # Return file content
28
- rescue => e
29
+ return file_data['data'] # Return file content
30
+ rescue StandardError => e
29
31
  raise NotationError, "Failed to download file: #{e.message}"
30
32
  end
31
33
  end
32
-
34
+
33
35
  when 'field'
34
36
  # Check if it's a TOTP field
35
37
  parameter = parsed[2].parameter&.first
36
- if parameter && parameter.downcase == 'onetimecode' && value.is_a?(String) && value.start_with?('otpauth://')
37
- if options[:generate_totp_code]
38
- begin
39
- totp_params = TOTP.parse_url(value)
40
- return TOTP.generate_code(
41
- totp_params['secret'],
42
- algorithm: totp_params['algorithm'],
43
- digits: totp_params['digits'],
44
- period: totp_params['period']
45
- )
46
- rescue => e
47
- raise NotationError, "Failed to generate TOTP code: #{e.message}"
48
- end
38
+ if parameter && parameter.downcase == 'onetimecode' && value.is_a?(String) && value.start_with?('otpauth://') && (options[:generate_totp_code])
39
+ begin
40
+ totp_params = TOTP.parse_url(value)
41
+ return TOTP.generate_code(
42
+ totp_params['secret'],
43
+ algorithm: totp_params['algorithm'],
44
+ digits: totp_params['digits'],
45
+ period: totp_params['period']
46
+ )
47
+ rescue StandardError => e
48
+ raise NotationError, "Failed to generate TOTP code: #{e.message}"
49
49
  end
50
50
  end
51
51
  end
52
-
52
+
53
53
  value
54
54
  end
55
-
55
+
56
56
  # Convenience method to get TOTP code directly
57
57
  def get_totp_code(notation)
58
58
  get_value(notation, auto_process: true, generate_totp_code: true)
59
59
  end
60
-
60
+
61
61
  # Convenience method to download file content directly
62
62
  def download_file(notation)
63
63
  get_value(notation, auto_process: true, auto_download: true)
64
64
  end
65
65
  end
66
66
  end
67
- end
67
+ end
@@ -6,18 +6,18 @@ module KeeperSecretsManager
6
6
  module Storage
7
7
  # Base storage interface
8
8
  module KeyValueStorage
9
- def get_string(key)
10
- raise NotImplementedError, "Subclass must implement get_string"
9
+ def get_string(_key)
10
+ raise NotImplementedError, 'Subclass must implement get_string'
11
11
  end
12
12
 
13
- def save_string(key, value)
14
- raise NotImplementedError, "Subclass must implement save_string"
13
+ def save_string(_key, _value)
14
+ raise NotImplementedError, 'Subclass must implement save_string'
15
15
  end
16
16
 
17
17
  def get_bytes(key)
18
18
  data = get_string(key)
19
19
  return nil unless data
20
-
20
+
21
21
  # Handle both standard and URL-safe base64
22
22
  begin
23
23
  # First try standard base64
@@ -28,7 +28,7 @@ module KeeperSecretsManager
28
28
  padding = 4 - (data.length % 4)
29
29
  padding = 0 if padding == 4
30
30
  Base64.urlsafe_decode64(data + '=' * padding)
31
- rescue => e
31
+ rescue StandardError => e
32
32
  # Last resort - try with decode64 which is more lenient
33
33
  Base64.decode64(data)
34
34
  end
@@ -39,8 +39,8 @@ module KeeperSecretsManager
39
39
  save_string(key, Base64.strict_encode64(value))
40
40
  end
41
41
 
42
- def delete(key)
43
- raise NotImplementedError, "Subclass must implement delete"
42
+ def delete(_key)
43
+ raise NotImplementedError, 'Subclass must implement delete'
44
44
  end
45
45
 
46
46
  def contains?(key)
@@ -54,7 +54,7 @@ module KeeperSecretsManager
54
54
 
55
55
  def initialize(config_data = nil)
56
56
  @data = {}
57
-
57
+
58
58
  # Initialize from JSON string, base64 string, or hash
59
59
  if config_data
60
60
  parsed = case config_data
@@ -70,7 +70,7 @@ module KeeperSecretsManager
70
70
  else
71
71
  {}
72
72
  end
73
-
73
+
74
74
  parsed.each { |k, v| @data[k.to_s] = v.to_s }
75
75
  end
76
76
  end
@@ -94,20 +94,20 @@ module KeeperSecretsManager
94
94
  def to_json(*args)
95
95
  @data.to_json(*args)
96
96
  end
97
-
97
+
98
98
  private
99
-
99
+
100
100
  def is_base64?(str)
101
101
  # Check if string is valid base64
102
102
  return false if str.nil? || str.empty?
103
-
103
+
104
104
  # Remove whitespace
105
105
  str = str.strip
106
-
106
+
107
107
  # Check if length is multiple of 4 (with padding) or can be padded to multiple of 4
108
108
  # Also check if it only contains base64 characters
109
- base64_regex = /\A[A-Za-z0-9+\/]*={0,2}\z/
110
-
109
+ base64_regex = %r{\A[A-Za-z0-9+/]*={0,2}\z}
110
+
111
111
  str.match?(base64_regex) && (str.length % 4 == 0 || str.length % 4 == 2 || str.length % 4 == 3)
112
112
  end
113
113
  end
@@ -143,11 +143,11 @@ module KeeperSecretsManager
143
143
  begin
144
144
  content = File.read(@filename)
145
145
  # Handle empty files
146
- if content.strip.empty?
147
- @data = {}
148
- else
149
- @data = JSON.parse(content)
150
- end
146
+ @data = if content.strip.empty?
147
+ {}
148
+ else
149
+ JSON.parse(content)
150
+ end
151
151
  rescue JSON::ParserError => e
152
152
  raise Error, "Failed to parse config file: #{e.message}"
153
153
  end
@@ -157,19 +157,20 @@ module KeeperSecretsManager
157
157
  def save_data
158
158
  # Ensure directory exists
159
159
  FileUtils.mkdir_p(File.dirname(@filename))
160
-
160
+
161
161
  # Write atomically to avoid corruption
162
162
  temp_file = "#{@filename}.tmp"
163
- File.open(temp_file, 'w') do |f|
163
+ # Create temp file with secure permissions (0600)
164
+ File.open(temp_file, 'w', 0o600) do |f|
164
165
  f.write(JSON.pretty_generate(@data))
165
166
  end
166
-
167
+
167
168
  # Move atomically
168
169
  File.rename(temp_file, @filename)
169
-
170
- # Set restrictive permissions (owner read/write only)
171
- File.chmod(0600, @filename)
172
- rescue => e
170
+
171
+ # Ensure final file has restrictive permissions (owner read/write only)
172
+ File.chmod(0o600, @filename)
173
+ rescue StandardError => e
173
174
  raise Error, "Failed to save config file: #{e.message}"
174
175
  end
175
176
  end
@@ -186,12 +187,12 @@ module KeeperSecretsManager
186
187
  ENV["#{@prefix}#{key.to_s.upcase}"]
187
188
  end
188
189
 
189
- def save_string(key, value)
190
- raise Error, "Environment storage is read-only"
190
+ def save_string(_key, _value)
191
+ raise Error, 'Environment storage is read-only'
191
192
  end
192
193
 
193
- def delete(key)
194
- raise Error, "Environment storage is read-only"
194
+ def delete(_key)
195
+ raise Error, 'Environment storage is read-only'
195
196
  end
196
197
  end
197
198
 
@@ -208,11 +209,9 @@ module KeeperSecretsManager
208
209
 
209
210
  def get_string(key)
210
211
  key_str = key.to_s
211
-
212
+
212
213
  # Check cache validity
213
- if @cache.key?(key_str) && !expired?(key_str)
214
- return @cache[key_str]
215
- end
214
+ return @cache[key_str] if @cache.key?(key_str) && !expired?(key_str)
216
215
 
217
216
  # Fetch from base storage
218
217
  value = @base_storage.get_string(key)
@@ -220,7 +219,7 @@ module KeeperSecretsManager
220
219
  @cache[key_str] = value
221
220
  @timestamps[key_str] = Time.now
222
221
  end
223
-
222
+
224
223
  value
225
224
  end
226
225
 
@@ -247,8 +246,9 @@ module KeeperSecretsManager
247
246
 
248
247
  def expired?(key)
249
248
  return true unless @timestamps[key]
249
+
250
250
  Time.now - @timestamps[key] > @ttl_seconds
251
251
  end
252
252
  end
253
253
  end
254
- end
254
+ end
@@ -12,7 +12,7 @@ module KeeperSecretsManager
12
12
  'SHA256' => OpenSSL::Digest::SHA256,
13
13
  'SHA512' => OpenSSL::Digest::SHA512
14
14
  }.freeze
15
-
15
+
16
16
  # Generate a TOTP code
17
17
  # @param secret [String] Base32 encoded secret
18
18
  # @param time [Time] Time to generate code for (default: current time)
@@ -23,45 +23,45 @@ module KeeperSecretsManager
23
23
  def self.generate_code(secret, time: Time.now, algorithm: 'SHA1', digits: 6, period: 30)
24
24
  # Validate inputs
25
25
  raise ArgumentError, "Invalid algorithm: #{algorithm}" unless ALGORITHMS.key?(algorithm)
26
- raise ArgumentError, "Digits must be 6 or 8" unless [6, 8].include?(digits)
27
- raise ArgumentError, "Period must be positive" unless period.positive?
28
-
26
+ raise ArgumentError, 'Digits must be 6 or 8' unless [6, 8].include?(digits)
27
+ raise ArgumentError, 'Period must be positive' unless period.positive?
28
+
29
29
  # Decode base32 secret
30
30
  key = Base32.decode(secret.upcase.tr(' ', ''))
31
-
31
+
32
32
  # Calculate time counter
33
33
  counter = (time.to_i / period).floor
34
-
34
+
35
35
  # Convert counter to 8-byte string (big-endian)
36
36
  counter_bytes = [counter].pack('Q>')
37
-
37
+
38
38
  # Generate HMAC
39
39
  digest = ALGORITHMS[algorithm].new
40
40
  hmac = OpenSSL::HMAC.digest(digest, key, counter_bytes)
41
-
41
+
42
42
  # Extract dynamic binary code
43
43
  offset = hmac[-1].ord & 0x0f
44
44
  code = (hmac[offset].ord & 0x7f) << 24 |
45
45
  (hmac[offset + 1].ord & 0xff) << 16 |
46
46
  (hmac[offset + 2].ord & 0xff) << 8 |
47
47
  (hmac[offset + 3].ord & 0xff)
48
-
48
+
49
49
  # Generate final OTP value
50
- otp = code % (10 ** digits)
51
-
50
+ otp = code % (10**digits)
51
+
52
52
  # Pad with leading zeros if necessary
53
53
  otp.to_s.rjust(digits, '0')
54
54
  end
55
-
55
+
56
56
  # Parse TOTP URL (otpauth://totp/...)
57
57
  # @param url [String] TOTP URL
58
58
  # @return [Hash] Parsed components
59
59
  def self.parse_url(url)
60
60
  uri = URI(url)
61
-
62
- raise ArgumentError, "Invalid TOTP URL scheme" unless uri.scheme == 'otpauth'
63
- raise ArgumentError, "Invalid TOTP URL type" unless uri.host == 'totp'
64
-
61
+
62
+ raise ArgumentError, 'Invalid TOTP URL scheme' unless uri.scheme == 'otpauth'
63
+ raise ArgumentError, 'Invalid TOTP URL type' unless uri.host == 'totp'
64
+
65
65
  # Extract label (issuer:account or just account)
66
66
  path = uri.path[1..-1] # Remove leading /
67
67
  if path.include?(':')
@@ -70,10 +70,10 @@ module KeeperSecretsManager
70
70
  account = path
71
71
  issuer = nil
72
72
  end
73
-
73
+
74
74
  # Parse query parameters
75
75
  params = URI.decode_www_form(uri.query || '').to_h
76
-
76
+
77
77
  {
78
78
  'account' => URI.decode_www_form_component(account || ''),
79
79
  'issuer' => issuer ? URI.decode_www_form_component(issuer) : params['issuer'],
@@ -83,7 +83,7 @@ module KeeperSecretsManager
83
83
  'period' => (params['period'] || '30').to_i
84
84
  }
85
85
  end
86
-
86
+
87
87
  # Generate TOTP URL
88
88
  # @param account [String] Account name (e.g., email)
89
89
  # @param secret [String] Base32 encoded secret
@@ -94,20 +94,20 @@ module KeeperSecretsManager
94
94
  # @return [String] TOTP URL
95
95
  def self.generate_url(account, secret, issuer: nil, algorithm: 'SHA1', digits: 6, period: 30)
96
96
  label = issuer ? "#{issuer}:#{account}" : account
97
-
97
+
98
98
  params = {
99
99
  'secret' => secret,
100
100
  'algorithm' => algorithm,
101
101
  'digits' => digits,
102
102
  'period' => period
103
103
  }
104
-
104
+
105
105
  params['issuer'] = issuer if issuer
106
-
106
+
107
107
  query = URI.encode_www_form(params)
108
108
  "otpauth://totp/#{URI.encode_www_form_component(label)}?#{query}"
109
109
  end
110
-
110
+
111
111
  # Validate a TOTP code
112
112
  # @param secret [String] Base32 encoded secret
113
113
  # @param code [String] Code to validate
@@ -122,13 +122,13 @@ module KeeperSecretsManager
122
122
  (-window..window).each do |offset|
123
123
  test_time = time + (offset * period)
124
124
  test_code = generate_code(secret, time: test_time, algorithm: algorithm, digits: digits, period: period)
125
-
125
+
126
126
  return true if test_code == code
127
127
  end
128
-
128
+
129
129
  false
130
130
  end
131
-
131
+
132
132
  # Generate a random secret suitable for TOTP
133
133
  # @param length [Integer] Number of bytes (default: 20 for 160 bits)
134
134
  # @return [String] Base32 encoded secret
@@ -137,4 +137,4 @@ module KeeperSecretsManager
137
137
  Base32.encode(random_bytes).delete('=')
138
138
  end
139
139
  end
140
- end
140
+ end
@@ -35,6 +35,7 @@ module KeeperSecretsManager
35
35
 
36
36
  # Base64 decode
37
37
  def base64_to_bytes(str)
38
+ raise Error, 'base64_to_bytes: received nil' if str.nil?
38
39
  Base64.strict_decode64(str)
39
40
  rescue ArgumentError => e
40
41
  raise Error, "Invalid base64: #{e.message}"
@@ -42,7 +43,7 @@ module KeeperSecretsManager
42
43
 
43
44
  # URL-safe base64 encode (with padding)
44
45
  def url_safe_str_to_bytes(str)
45
- # Add padding if needed
46
+ raise Error, 'url_safe_str_to_bytes: received nil' if str.nil?
46
47
  str += '=' * (4 - str.length % 4) if str.length % 4 != 0
47
48
  Base64.urlsafe_decode64(str)
48
49
  end
@@ -67,6 +68,75 @@ module KeeperSecretsManager
67
68
  generate_random_bytes(16)
68
69
  end
69
70
 
71
+ # Generate a cryptographically secure random password
72
+ #
73
+ # @param length [Integer] Total password length (default: 64)
74
+ # @param lowercase [Integer] Minimum number of lowercase letters (default: 0)
75
+ # @param uppercase [Integer] Minimum number of uppercase letters (default: 0)
76
+ # @param digits [Integer] Minimum number of digit characters (default: 0)
77
+ # @param special_characters [Integer] Minimum number of special characters (default: 0)
78
+ # @return [String] Generated password
79
+ # @raise [ArgumentError] If parameters are invalid or minimums exceed length
80
+ #
81
+ # @example Generate a default 64-character password
82
+ # password = KeeperSecretsManager::Utils.generate_password
83
+ # # => "Xk9$mP2...64 chars total"
84
+ #
85
+ # @example Generate a 32-character password with specific requirements
86
+ # password = KeeperSecretsManager::Utils.generate_password(
87
+ # length: 32,
88
+ # lowercase: 2,
89
+ # uppercase: 2,
90
+ # digits: 2,
91
+ # special_characters: 2
92
+ # )
93
+ # # => "aB12$...32 chars with at least 2 of each type"
94
+ #
95
+ # @example Use with record update
96
+ # record = secrets_manager.get_secrets(['RECORD_UID']).first
97
+ # record.password = KeeperSecretsManager::Utils.generate_password(length: 20)
98
+ # secrets_manager.update_secret(record)
99
+ def generate_password(length: 64, lowercase: 0, uppercase: 0, digits: 0, special_characters: 0)
100
+ # Validate inputs
101
+ raise ArgumentError, 'Length must be positive' if length <= 0
102
+ raise ArgumentError, 'Character counts must be non-negative' if [lowercase, uppercase, digits, special_characters].any?(&:negative?)
103
+
104
+ total_minimums = lowercase + uppercase + digits + special_characters
105
+ raise ArgumentError, "Sum of character minimums (#{total_minimums}) cannot exceed password length (#{length})" if total_minimums > length
106
+
107
+ # Character sets
108
+ lowercase_chars = 'abcdefghijklmnopqrstuvwxyz'
109
+ uppercase_chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
110
+ digit_chars = '0123456789'
111
+ special_chars = '!@#$%^&*()_+-=[]{}|;:,.<>?'
112
+
113
+ # Build password character array
114
+ password_chars = []
115
+
116
+ # Add minimum required characters from each category
117
+ lowercase.times { password_chars << lowercase_chars[SecureRandom.random_number(lowercase_chars.length)] }
118
+ uppercase.times { password_chars << uppercase_chars[SecureRandom.random_number(uppercase_chars.length)] }
119
+ digits.times { password_chars << digit_chars[SecureRandom.random_number(digit_chars.length)] }
120
+ special_characters.times { password_chars << special_chars[SecureRandom.random_number(special_chars.length)] }
121
+
122
+ # Fill remaining length with random characters from all categories
123
+ remaining = length - total_minimums
124
+ all_chars = lowercase_chars + uppercase_chars + digit_chars + special_chars
125
+
126
+ remaining.times do
127
+ password_chars << all_chars[SecureRandom.random_number(all_chars.length)]
128
+ end
129
+
130
+ # Shuffle using Fisher-Yates algorithm with SecureRandom for cryptographic security
131
+ # This ensures minimum characters aren't clustered at the beginning
132
+ (password_chars.length - 1).downto(1) do |i|
133
+ j = SecureRandom.random_number(i + 1)
134
+ password_chars[i], password_chars[j] = password_chars[j], password_chars[i]
135
+ end
136
+
137
+ password_chars.join
138
+ end
139
+
70
140
  # Get current time in milliseconds
71
141
  def now_milliseconds
72
142
  (Time.now.to_f * 1000).to_i
@@ -75,7 +145,7 @@ module KeeperSecretsManager
75
145
  # Convert string to boolean
76
146
  def strtobool(val)
77
147
  return val if val.is_a?(TrueClass) || val.is_a?(FalseClass)
78
-
148
+
79
149
  val_str = val.to_s.downcase.strip
80
150
  case val_str
81
151
  when 'true', '1', 'yes', 'y', 'on'
@@ -94,7 +164,7 @@ module KeeperSecretsManager
94
164
 
95
165
  # Deep merge hashes
96
166
  def deep_merge(hash1, hash2)
97
- hash1.merge(hash2) do |key, old_val, new_val|
167
+ hash1.merge(hash2) do |_key, old_val, new_val|
98
168
  if old_val.is_a?(Hash) && new_val.is_a?(Hash)
99
169
  deep_merge(old_val, new_val)
100
170
  else
@@ -112,10 +182,9 @@ module KeeperSecretsManager
112
182
 
113
183
  # Convert snake_case to camelCase
114
184
  def snake_to_camel(str, capitalize_first = false)
115
- result = str.split('_').map.with_index do |word, i|
185
+ str.split('_').map.with_index do |word, i|
116
186
  i == 0 && !capitalize_first ? word : word.capitalize
117
187
  end.join
118
- result
119
188
  end
120
189
 
121
190
  # Safe integer conversion
@@ -135,10 +204,10 @@ module KeeperSecretsManager
135
204
  # Parse server URL from hostname
136
205
  def get_server_url(hostname, use_ssl = true)
137
206
  return nil if blank?(hostname)
138
-
207
+
139
208
  # Remove protocol if present
140
209
  hostname = hostname.sub(%r{^https?://}, '')
141
-
210
+
142
211
  # Build URL
143
212
  protocol = use_ssl ? 'https' : 'http'
144
213
  "#{protocol}://#{hostname}"
@@ -151,13 +220,13 @@ module KeeperSecretsManager
151
220
  parts = token_or_hostname.split(':')
152
221
  return parts[0].upcase if parts.length >= 2
153
222
  end
154
-
223
+
155
224
  # Check if hostname matches a known region
156
225
  hostname = token_or_hostname.to_s.downcase
157
226
  KeeperGlobals::KEEPER_SERVERS.each do |region, server|
158
227
  return region if hostname.include?(server)
159
228
  end
160
-
229
+
161
230
  # Default to US
162
231
  'US'
163
232
  end
@@ -165,12 +234,12 @@ module KeeperSecretsManager
165
234
  # Validate UID format
166
235
  def valid_uid?(uid)
167
236
  return false if blank?(uid)
168
-
237
+
169
238
  # UIDs are base64url encoded 16-byte values
170
239
  begin
171
240
  bytes = url_safe_str_to_bytes(uid)
172
241
  bytes.length == 16
173
- rescue
242
+ rescue StandardError
174
243
  false
175
244
  end
176
245
  end
@@ -180,17 +249,15 @@ module KeeperSecretsManager
180
249
  attempt = 0
181
250
  begin
182
251
  yield
183
- rescue => e
252
+ rescue StandardError => e
184
253
  attempt += 1
185
- if attempt >= max_attempts
186
- raise e
187
- end
188
-
189
- delay = [base_delay * (2 ** (attempt - 1)), max_delay].min
254
+ raise e if attempt >= max_attempts
255
+
256
+ delay = [base_delay * (2**(attempt - 1)), max_delay].min
190
257
  sleep(delay)
191
258
  retry
192
259
  end
193
260
  end
194
261
  end
195
262
  end
196
- end
263
+ end
@@ -1,3 +1,3 @@
1
1
  module KeeperSecretsManager
2
- VERSION = '17.0.4'.freeze
3
- end
2
+ VERSION = '17.2.0'.freeze
3
+ end
@@ -8,6 +8,8 @@ require 'keeper_secrets_manager/storage'
8
8
  require 'keeper_secrets_manager/dto'
9
9
  require 'keeper_secrets_manager/field_types'
10
10
  require 'keeper_secrets_manager/notation'
11
+ require 'keeper_secrets_manager/notation_enhancements'
12
+ require 'keeper_secrets_manager/cache'
11
13
  require 'keeper_secrets_manager/core'
12
14
  require 'keeper_secrets_manager/folder_manager'
13
15
 
@@ -24,15 +26,21 @@ module KeeperSecretsManager
24
26
  def self.new(options = {})
25
27
  Core::SecretsManager.new(options)
26
28
  end
27
-
29
+
28
30
  # Convenience method to create from token
29
31
  def self.from_token(token, options = {})
30
32
  Core::SecretsManager.new(options.merge(token: token))
31
33
  end
32
-
34
+
35
+ # Convenience method to create from base64 config string
36
+ def self.from_config(config_base64, options = {})
37
+ storage = Storage::InMemoryStorage.new(config_base64)
38
+ Core::SecretsManager.new(options.merge(config: storage))
39
+ end
40
+
33
41
  # Convenience method to create from config file
34
42
  def self.from_file(filename, options = {})
35
43
  storage = Storage::FileStorage.new(filename)
36
44
  Core::SecretsManager.new(options.merge(config: storage))
37
45
  end
38
- end
46
+ end