Module: KeeperSecretsManager::Utils

Defined in:
lib/keeper_secrets_manager/utils.rb

Class Method Summary collapse

Class Method Details

.base64_to_bytes(str) ⇒ Object

Base64 decode



37
38
39
40
41
# File 'lib/keeper_secrets_manager/utils.rb', line 37

def base64_to_bytes(str)
  Base64.strict_decode64(str)
rescue ArgumentError => e
  raise Error, "Invalid base64: #{e.message}"
end

.blank?(str) ⇒ Boolean

Check if string is blank

Returns:

  • (Boolean)


160
161
162
# File 'lib/keeper_secrets_manager/utils.rb', line 160

def blank?(str)
  str.nil? || str.strip.empty?
end

.bytes_to_base64(bytes) ⇒ Object

Base64 encode



32
33
34
# File 'lib/keeper_secrets_manager/utils.rb', line 32

def bytes_to_base64(bytes)
  Base64.strict_encode64(bytes)
end

.bytes_to_string(bytes) ⇒ Object

Convert bytes to string



15
16
17
# File 'lib/keeper_secrets_manager/utils.rb', line 15

def bytes_to_string(bytes)
  bytes.force_encoding('UTF-8')
end

.bytes_to_url_safe_str(bytes) ⇒ Object

URL-safe base64 decode (without padding)



51
52
53
# File 'lib/keeper_secrets_manager/utils.rb', line 51

def bytes_to_url_safe_str(bytes)
  Base64.urlsafe_encode64(bytes).delete('=')
end

.camel_to_snake(str) ⇒ Object

Convert camelCase to snake_case



176
177
178
179
180
# File 'lib/keeper_secrets_manager/utils.rb', line 176

def camel_to_snake(str)
  str.gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
     .gsub(/([a-z\d])([A-Z])/, '\1_\2')
     .downcase
end

.deep_merge(hash1, hash2) ⇒ Object

Deep merge hashes



165
166
167
168
169
170
171
172
173
# File 'lib/keeper_secrets_manager/utils.rb', line 165

def deep_merge(hash1, hash2)
  hash1.merge(hash2) do |_key, old_val, new_val|
    if old_val.is_a?(Hash) && new_val.is_a?(Hash)
      deep_merge(old_val, new_val)
    else
      new_val
    end
  end
end

.dict_to_json(obj) ⇒ Object

Convert hash/object to JSON string



20
21
22
# File 'lib/keeper_secrets_manager/utils.rb', line 20

def dict_to_json(obj)
  JSON.generate(obj)
end

.extract_region(token_or_hostname) ⇒ Object

Extract region from token or hostname



216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
# File 'lib/keeper_secrets_manager/utils.rb', line 216

def extract_region(token_or_hostname)
  # Check if it's a token with region prefix
  if token_or_hostname&.include?(':')
    parts = token_or_hostname.split(':')
    return parts[0].upcase if parts.length >= 2
  end

  # Check if hostname matches a known region
  hostname = token_or_hostname.to_s.downcase
  KeeperGlobals::KEEPER_SERVERS.each do |region, server|
    return region if hostname.include?(server)
  end

  # Default to US
  'US'
end

.generate_password(length: 64, lowercase: 0, uppercase: 0, digits: 0, special_characters: 0) ⇒ String

Generate a cryptographically secure random password

Examples:

Generate a default 64-character password

password = KeeperSecretsManager::Utils.generate_password
# => "Xk9$mP2...64 chars total"

Generate a 32-character password with specific requirements

password = KeeperSecretsManager::Utils.generate_password(
  length: 32,
  lowercase: 2,
  uppercase: 2,
  digits: 2,
  special_characters: 2
)
# => "aB12$...32 chars with at least 2 of each type"

Use with record update

record = secrets_manager.get_secrets(['RECORD_UID']).first
record.password = KeeperSecretsManager::Utils.generate_password(length: 20)
secrets_manager.update_secret(record)

Parameters:

  • length (Integer) (defaults to: 64)

    Total password length (default: 64)

  • lowercase (Integer) (defaults to: 0)

    Minimum number of lowercase letters (default: 0)

  • uppercase (Integer) (defaults to: 0)

    Minimum number of uppercase letters (default: 0)

  • digits (Integer) (defaults to: 0)

    Minimum number of digit characters (default: 0)

  • special_characters (Integer) (defaults to: 0)

    Minimum number of special characters (default: 0)

Returns:

  • (String)

    Generated password

Raises:

  • (ArgumentError)

    If parameters are invalid or minimums exceed length



98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
# File 'lib/keeper_secrets_manager/utils.rb', line 98

def generate_password(length: 64, lowercase: 0, uppercase: 0, digits: 0, special_characters: 0)
  # Validate inputs
  raise ArgumentError, 'Length must be positive' if length <= 0
  raise ArgumentError, 'Character counts must be non-negative' if [lowercase, uppercase, digits, special_characters].any?(&:negative?)

  total_minimums = lowercase + uppercase + digits + special_characters
  raise ArgumentError, "Sum of character minimums (#{total_minimums}) cannot exceed password length (#{length})" if total_minimums > length

  # Character sets
  lowercase_chars = 'abcdefghijklmnopqrstuvwxyz'
  uppercase_chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
  digit_chars = '0123456789'
  special_chars = '!@#$%^&*()_+-=[]{}|;:,.<>?'

  # Build password character array
  password_chars = []

  # Add minimum required characters from each category
  lowercase.times { password_chars << lowercase_chars[SecureRandom.random_number(lowercase_chars.length)] }
  uppercase.times { password_chars << uppercase_chars[SecureRandom.random_number(uppercase_chars.length)] }
  digits.times { password_chars << digit_chars[SecureRandom.random_number(digit_chars.length)] }
  special_characters.times { password_chars << special_chars[SecureRandom.random_number(special_chars.length)] }

  # Fill remaining length with random characters from all categories
  remaining = length - total_minimums
  all_chars = lowercase_chars + uppercase_chars + digit_chars + special_chars

  remaining.times do
    password_chars << all_chars[SecureRandom.random_number(all_chars.length)]
  end

  # Shuffle using Fisher-Yates algorithm with SecureRandom for cryptographic security
  # This ensures minimum characters aren't clustered at the beginning
  (password_chars.length - 1).downto(1) do |i|
    j = SecureRandom.random_number(i + 1)
    password_chars[i], password_chars[j] = password_chars[j], password_chars[i]
  end

  password_chars.join
end

.generate_random_bytes(length) ⇒ Object

Generate random bytes



56
57
58
# File 'lib/keeper_secrets_manager/utils.rb', line 56

def generate_random_bytes(length)
  SecureRandom.random_bytes(length)
end

.generate_uidObject

Generate UID (16 random bytes)



61
62
63
# File 'lib/keeper_secrets_manager/utils.rb', line 61

def generate_uid
  bytes_to_url_safe_str(generate_random_bytes(16))
end

.generate_uid_bytesObject

Generate UID bytes



66
67
68
# File 'lib/keeper_secrets_manager/utils.rb', line 66

def generate_uid_bytes
  generate_random_bytes(16)
end

.get_server_url(hostname, use_ssl = true) ⇒ Object

Parse server URL from hostname



204
205
206
207
208
209
210
211
212
213
# File 'lib/keeper_secrets_manager/utils.rb', line 204

def get_server_url(hostname, use_ssl = true)
  return nil if blank?(hostname)

  # Remove protocol if present
  hostname = hostname.sub(%r{^https?://}, '')

  # Build URL
  protocol = use_ssl ? 'https' : 'http'
  "#{protocol}://#{hostname}"
end

.json_to_dict(json_str) ⇒ Object

Parse JSON string to hash



25
26
27
28
29
# File 'lib/keeper_secrets_manager/utils.rb', line 25

def json_to_dict(json_str)
  JSON.parse(json_str)
rescue JSON::ParserError => e
  raise Error, "Invalid JSON: #{e.message}"
end

.now_millisecondsObject

Get current time in milliseconds



140
141
142
# File 'lib/keeper_secrets_manager/utils.rb', line 140

def now_milliseconds
  (Time.now.to_f * 1000).to_i
end

.retry_with_backoff(max_attempts: 3, base_delay: 1, max_delay: 60) ⇒ Object

Retry with exponential backoff



247
248
249
250
251
252
253
254
255
256
257
258
259
# File 'lib/keeper_secrets_manager/utils.rb', line 247

def retry_with_backoff(max_attempts: 3, base_delay: 1, max_delay: 60)
  attempt = 0
  begin
    yield
  rescue StandardError => e
    attempt += 1
    raise e if attempt >= max_attempts

    delay = [base_delay * (2**(attempt - 1)), max_delay].min
    sleep(delay)
    retry
  end
end

.snake_to_camel(str, capitalize_first = false) ⇒ Object

Convert snake_case to camelCase



183
184
185
186
187
# File 'lib/keeper_secrets_manager/utils.rb', line 183

def snake_to_camel(str, capitalize_first = false)
  str.split('_').map.with_index do |word, i|
    i == 0 && !capitalize_first ? word : word.capitalize
  end.join
end

.string_to_bytes(str) ⇒ Object

Convert string to bytes



10
11
12
# File 'lib/keeper_secrets_manager/utils.rb', line 10

def string_to_bytes(str)
  str.b
end

.strtobool(val) ⇒ Object

Convert string to boolean



145
146
147
148
149
150
151
152
153
154
155
156
157
# File 'lib/keeper_secrets_manager/utils.rb', line 145

def strtobool(val)
  return val if val.is_a?(TrueClass) || val.is_a?(FalseClass)

  val_str = val.to_s.downcase.strip
  case val_str
  when 'true', '1', 'yes', 'y', 'on'
    true
  when 'false', '0', 'no', 'n', 'off', ''
    false
  else
    raise ArgumentError, "Invalid boolean value: #{val}"
  end
end

.to_int(val, default = nil) ⇒ Object

Safe integer conversion



190
191
192
193
194
# File 'lib/keeper_secrets_manager/utils.rb', line 190

def to_int(val, default = nil)
  Integer(val)
rescue ArgumentError, TypeError
  default
end

.url_join(*parts) ⇒ Object

URL join



197
198
199
200
201
# File 'lib/keeper_secrets_manager/utils.rb', line 197

def url_join(*parts)
  parts.map { |part| part.to_s.gsub(%r{^/+|/+$}, '') }
       .reject(&:empty?)
       .join('/')
end

.url_safe_str_to_bytes(str) ⇒ Object

URL-safe base64 encode (with padding)



44
45
46
47
48
# File 'lib/keeper_secrets_manager/utils.rb', line 44

def url_safe_str_to_bytes(str)
  # Add padding if needed
  str += '=' * (4 - str.length % 4) if str.length % 4 != 0
  Base64.urlsafe_decode64(str)
end

.valid_uid?(uid) ⇒ Boolean

Validate UID format

Returns:

  • (Boolean)


234
235
236
237
238
239
240
241
242
243
244
# File 'lib/keeper_secrets_manager/utils.rb', line 234

def valid_uid?(uid)
  return false if blank?(uid)

  # UIDs are base64url encoded 16-byte values
  begin
    bytes = url_safe_str_to_bytes(uid)
    bytes.length == 16
  rescue StandardError
    false
  end
end