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
42
# File 'lib/keeper_secrets_manager/utils.rb', line 37

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

.blank?(str) ⇒ Boolean

Check if string is blank

Returns:

  • (Boolean)


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

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)



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

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

.camel_to_snake(str) ⇒ Object

Convert camelCase to snake_case



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

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



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

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



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

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



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
138
# File 'lib/keeper_secrets_manager/utils.rb', line 99

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



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

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

.generate_uidObject

Generate UID (16 random bytes)



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

def generate_uid
  bytes_to_url_safe_str(generate_random_bytes(16))
end

.generate_uid_bytesObject

Generate UID bytes



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

def generate_uid_bytes
  generate_random_bytes(16)
end

.get_server_url(hostname, use_ssl = true) ⇒ Object

Parse server URL from hostname



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

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



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

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



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

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



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

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



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

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



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

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

.url_join(*parts) ⇒ Object

URL join



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

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)

Raises:



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

def url_safe_str_to_bytes(str)
  raise Error, 'url_safe_str_to_bytes: received nil' if str.nil?
  str += '=' * (4 - str.length % 4) if str.length % 4 != 0
  Base64.urlsafe_decode64(str)
end

.valid_uid?(uid) ⇒ Boolean

Validate UID format

Returns:

  • (Boolean)


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

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