keeper_secrets_manager 17.0.3 → 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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +26 -15
- data/Gemfile +3 -3
- data/README.md +1 -1
- data/Rakefile +1 -1
- data/lib/keeper_secrets_manager/config_keys.rb +2 -2
- data/lib/keeper_secrets_manager/core.rb +594 -394
- data/lib/keeper_secrets_manager/crypto.rb +106 -113
- data/lib/keeper_secrets_manager/dto/payload.rb +4 -4
- data/lib/keeper_secrets_manager/dto.rb +50 -32
- data/lib/keeper_secrets_manager/errors.rb +13 -2
- data/lib/keeper_secrets_manager/field_types.rb +3 -3
- data/lib/keeper_secrets_manager/folder_manager.rb +25 -29
- data/lib/keeper_secrets_manager/keeper_globals.rb +9 -15
- data/lib/keeper_secrets_manager/notation.rb +99 -92
- data/lib/keeper_secrets_manager/notation_enhancements.rb +22 -24
- data/lib/keeper_secrets_manager/storage.rb +35 -36
- data/lib/keeper_secrets_manager/totp.rb +27 -27
- data/lib/keeper_secrets_manager/utils.rb +83 -17
- data/lib/keeper_secrets_manager/version.rb +2 -2
- data/lib/keeper_secrets_manager.rb +3 -3
- metadata +7 -18
- data/examples/basic_usage.rb +0 -139
- data/examples/config_string_example.rb +0 -99
- data/examples/debug_secrets.rb +0 -84
- data/examples/demo_list_secrets.rb +0 -182
- data/examples/download_files.rb +0 -100
- data/examples/flexible_records_example.rb +0 -94
- data/examples/folder_hierarchy_demo.rb +0 -109
- data/examples/full_demo.rb +0 -176
- data/examples/my_test_standalone.rb +0 -176
- data/examples/simple_test.rb +0 -162
- data/examples/storage_examples.rb +0 -126
|
@@ -6,18 +6,18 @@ module KeeperSecretsManager
|
|
|
6
6
|
module Storage
|
|
7
7
|
# Base storage interface
|
|
8
8
|
module KeyValueStorage
|
|
9
|
-
def get_string(
|
|
10
|
-
raise NotImplementedError,
|
|
9
|
+
def get_string(_key)
|
|
10
|
+
raise NotImplementedError, 'Subclass must implement get_string'
|
|
11
11
|
end
|
|
12
12
|
|
|
13
|
-
def save_string(
|
|
14
|
-
raise NotImplementedError,
|
|
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(
|
|
43
|
-
raise NotImplementedError,
|
|
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 =
|
|
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
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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,19 @@ 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
163
|
File.open(temp_file, 'w') do |f|
|
|
164
164
|
f.write(JSON.pretty_generate(@data))
|
|
165
165
|
end
|
|
166
|
-
|
|
166
|
+
|
|
167
167
|
# Move atomically
|
|
168
168
|
File.rename(temp_file, @filename)
|
|
169
|
-
|
|
169
|
+
|
|
170
170
|
# Set restrictive permissions (owner read/write only)
|
|
171
|
-
File.chmod(
|
|
172
|
-
rescue => e
|
|
171
|
+
File.chmod(0o600, @filename)
|
|
172
|
+
rescue StandardError => e
|
|
173
173
|
raise Error, "Failed to save config file: #{e.message}"
|
|
174
174
|
end
|
|
175
175
|
end
|
|
@@ -186,12 +186,12 @@ module KeeperSecretsManager
|
|
|
186
186
|
ENV["#{@prefix}#{key.to_s.upcase}"]
|
|
187
187
|
end
|
|
188
188
|
|
|
189
|
-
def save_string(
|
|
190
|
-
raise Error,
|
|
189
|
+
def save_string(_key, _value)
|
|
190
|
+
raise Error, 'Environment storage is read-only'
|
|
191
191
|
end
|
|
192
192
|
|
|
193
|
-
def delete(
|
|
194
|
-
raise Error,
|
|
193
|
+
def delete(_key)
|
|
194
|
+
raise Error, 'Environment storage is read-only'
|
|
195
195
|
end
|
|
196
196
|
end
|
|
197
197
|
|
|
@@ -208,11 +208,9 @@ module KeeperSecretsManager
|
|
|
208
208
|
|
|
209
209
|
def get_string(key)
|
|
210
210
|
key_str = key.to_s
|
|
211
|
-
|
|
211
|
+
|
|
212
212
|
# Check cache validity
|
|
213
|
-
if @cache.key?(key_str) && !expired?(key_str)
|
|
214
|
-
return @cache[key_str]
|
|
215
|
-
end
|
|
213
|
+
return @cache[key_str] if @cache.key?(key_str) && !expired?(key_str)
|
|
216
214
|
|
|
217
215
|
# Fetch from base storage
|
|
218
216
|
value = @base_storage.get_string(key)
|
|
@@ -220,7 +218,7 @@ module KeeperSecretsManager
|
|
|
220
218
|
@cache[key_str] = value
|
|
221
219
|
@timestamps[key_str] = Time.now
|
|
222
220
|
end
|
|
223
|
-
|
|
221
|
+
|
|
224
222
|
value
|
|
225
223
|
end
|
|
226
224
|
|
|
@@ -247,8 +245,9 @@ module KeeperSecretsManager
|
|
|
247
245
|
|
|
248
246
|
def expired?(key)
|
|
249
247
|
return true unless @timestamps[key]
|
|
248
|
+
|
|
250
249
|
Time.now - @timestamps[key] > @ttl_seconds
|
|
251
250
|
end
|
|
252
251
|
end
|
|
253
252
|
end
|
|
254
|
-
end
|
|
253
|
+
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,
|
|
27
|
-
raise ArgumentError,
|
|
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
|
|
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,
|
|
63
|
-
raise ArgumentError,
|
|
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
|
|
@@ -67,6 +67,75 @@ module KeeperSecretsManager
|
|
|
67
67
|
generate_random_bytes(16)
|
|
68
68
|
end
|
|
69
69
|
|
|
70
|
+
# Generate a cryptographically secure random password
|
|
71
|
+
#
|
|
72
|
+
# @param length [Integer] Total password length (default: 64)
|
|
73
|
+
# @param lowercase [Integer] Minimum number of lowercase letters (default: 0)
|
|
74
|
+
# @param uppercase [Integer] Minimum number of uppercase letters (default: 0)
|
|
75
|
+
# @param digits [Integer] Minimum number of digit characters (default: 0)
|
|
76
|
+
# @param special_characters [Integer] Minimum number of special characters (default: 0)
|
|
77
|
+
# @return [String] Generated password
|
|
78
|
+
# @raise [ArgumentError] If parameters are invalid or minimums exceed length
|
|
79
|
+
#
|
|
80
|
+
# @example Generate a default 64-character password
|
|
81
|
+
# password = KeeperSecretsManager::Utils.generate_password
|
|
82
|
+
# # => "Xk9$mP2...64 chars total"
|
|
83
|
+
#
|
|
84
|
+
# @example Generate a 32-character password with specific requirements
|
|
85
|
+
# password = KeeperSecretsManager::Utils.generate_password(
|
|
86
|
+
# length: 32,
|
|
87
|
+
# lowercase: 2,
|
|
88
|
+
# uppercase: 2,
|
|
89
|
+
# digits: 2,
|
|
90
|
+
# special_characters: 2
|
|
91
|
+
# )
|
|
92
|
+
# # => "aB12$...32 chars with at least 2 of each type"
|
|
93
|
+
#
|
|
94
|
+
# @example Use with record update
|
|
95
|
+
# record = secrets_manager.get_secrets(['RECORD_UID']).first
|
|
96
|
+
# record.password = KeeperSecretsManager::Utils.generate_password(length: 20)
|
|
97
|
+
# secrets_manager.update_secret(record)
|
|
98
|
+
def generate_password(length: 64, lowercase: 0, uppercase: 0, digits: 0, special_characters: 0)
|
|
99
|
+
# Validate inputs
|
|
100
|
+
raise ArgumentError, 'Length must be positive' if length <= 0
|
|
101
|
+
raise ArgumentError, 'Character counts must be non-negative' if [lowercase, uppercase, digits, special_characters].any?(&:negative?)
|
|
102
|
+
|
|
103
|
+
total_minimums = lowercase + uppercase + digits + special_characters
|
|
104
|
+
raise ArgumentError, "Sum of character minimums (#{total_minimums}) cannot exceed password length (#{length})" if total_minimums > length
|
|
105
|
+
|
|
106
|
+
# Character sets
|
|
107
|
+
lowercase_chars = 'abcdefghijklmnopqrstuvwxyz'
|
|
108
|
+
uppercase_chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
|
|
109
|
+
digit_chars = '0123456789'
|
|
110
|
+
special_chars = '!@#$%^&*()_+-=[]{}|;:,.<>?'
|
|
111
|
+
|
|
112
|
+
# Build password character array
|
|
113
|
+
password_chars = []
|
|
114
|
+
|
|
115
|
+
# Add minimum required characters from each category
|
|
116
|
+
lowercase.times { password_chars << lowercase_chars[SecureRandom.random_number(lowercase_chars.length)] }
|
|
117
|
+
uppercase.times { password_chars << uppercase_chars[SecureRandom.random_number(uppercase_chars.length)] }
|
|
118
|
+
digits.times { password_chars << digit_chars[SecureRandom.random_number(digit_chars.length)] }
|
|
119
|
+
special_characters.times { password_chars << special_chars[SecureRandom.random_number(special_chars.length)] }
|
|
120
|
+
|
|
121
|
+
# Fill remaining length with random characters from all categories
|
|
122
|
+
remaining = length - total_minimums
|
|
123
|
+
all_chars = lowercase_chars + uppercase_chars + digit_chars + special_chars
|
|
124
|
+
|
|
125
|
+
remaining.times do
|
|
126
|
+
password_chars << all_chars[SecureRandom.random_number(all_chars.length)]
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Shuffle using Fisher-Yates algorithm with SecureRandom for cryptographic security
|
|
130
|
+
# This ensures minimum characters aren't clustered at the beginning
|
|
131
|
+
(password_chars.length - 1).downto(1) do |i|
|
|
132
|
+
j = SecureRandom.random_number(i + 1)
|
|
133
|
+
password_chars[i], password_chars[j] = password_chars[j], password_chars[i]
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
password_chars.join
|
|
137
|
+
end
|
|
138
|
+
|
|
70
139
|
# Get current time in milliseconds
|
|
71
140
|
def now_milliseconds
|
|
72
141
|
(Time.now.to_f * 1000).to_i
|
|
@@ -75,7 +144,7 @@ module KeeperSecretsManager
|
|
|
75
144
|
# Convert string to boolean
|
|
76
145
|
def strtobool(val)
|
|
77
146
|
return val if val.is_a?(TrueClass) || val.is_a?(FalseClass)
|
|
78
|
-
|
|
147
|
+
|
|
79
148
|
val_str = val.to_s.downcase.strip
|
|
80
149
|
case val_str
|
|
81
150
|
when 'true', '1', 'yes', 'y', 'on'
|
|
@@ -94,7 +163,7 @@ module KeeperSecretsManager
|
|
|
94
163
|
|
|
95
164
|
# Deep merge hashes
|
|
96
165
|
def deep_merge(hash1, hash2)
|
|
97
|
-
hash1.merge(hash2) do |
|
|
166
|
+
hash1.merge(hash2) do |_key, old_val, new_val|
|
|
98
167
|
if old_val.is_a?(Hash) && new_val.is_a?(Hash)
|
|
99
168
|
deep_merge(old_val, new_val)
|
|
100
169
|
else
|
|
@@ -112,10 +181,9 @@ module KeeperSecretsManager
|
|
|
112
181
|
|
|
113
182
|
# Convert snake_case to camelCase
|
|
114
183
|
def snake_to_camel(str, capitalize_first = false)
|
|
115
|
-
|
|
184
|
+
str.split('_').map.with_index do |word, i|
|
|
116
185
|
i == 0 && !capitalize_first ? word : word.capitalize
|
|
117
186
|
end.join
|
|
118
|
-
result
|
|
119
187
|
end
|
|
120
188
|
|
|
121
189
|
# Safe integer conversion
|
|
@@ -135,10 +203,10 @@ module KeeperSecretsManager
|
|
|
135
203
|
# Parse server URL from hostname
|
|
136
204
|
def get_server_url(hostname, use_ssl = true)
|
|
137
205
|
return nil if blank?(hostname)
|
|
138
|
-
|
|
206
|
+
|
|
139
207
|
# Remove protocol if present
|
|
140
208
|
hostname = hostname.sub(%r{^https?://}, '')
|
|
141
|
-
|
|
209
|
+
|
|
142
210
|
# Build URL
|
|
143
211
|
protocol = use_ssl ? 'https' : 'http'
|
|
144
212
|
"#{protocol}://#{hostname}"
|
|
@@ -151,13 +219,13 @@ module KeeperSecretsManager
|
|
|
151
219
|
parts = token_or_hostname.split(':')
|
|
152
220
|
return parts[0].upcase if parts.length >= 2
|
|
153
221
|
end
|
|
154
|
-
|
|
222
|
+
|
|
155
223
|
# Check if hostname matches a known region
|
|
156
224
|
hostname = token_or_hostname.to_s.downcase
|
|
157
225
|
KeeperGlobals::KEEPER_SERVERS.each do |region, server|
|
|
158
226
|
return region if hostname.include?(server)
|
|
159
227
|
end
|
|
160
|
-
|
|
228
|
+
|
|
161
229
|
# Default to US
|
|
162
230
|
'US'
|
|
163
231
|
end
|
|
@@ -165,12 +233,12 @@ module KeeperSecretsManager
|
|
|
165
233
|
# Validate UID format
|
|
166
234
|
def valid_uid?(uid)
|
|
167
235
|
return false if blank?(uid)
|
|
168
|
-
|
|
236
|
+
|
|
169
237
|
# UIDs are base64url encoded 16-byte values
|
|
170
238
|
begin
|
|
171
239
|
bytes = url_safe_str_to_bytes(uid)
|
|
172
240
|
bytes.length == 16
|
|
173
|
-
rescue
|
|
241
|
+
rescue StandardError
|
|
174
242
|
false
|
|
175
243
|
end
|
|
176
244
|
end
|
|
@@ -180,17 +248,15 @@ module KeeperSecretsManager
|
|
|
180
248
|
attempt = 0
|
|
181
249
|
begin
|
|
182
250
|
yield
|
|
183
|
-
rescue => e
|
|
251
|
+
rescue StandardError => e
|
|
184
252
|
attempt += 1
|
|
185
|
-
if attempt >= max_attempts
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
delay = [base_delay * (2 ** (attempt - 1)), max_delay].min
|
|
253
|
+
raise e if attempt >= max_attempts
|
|
254
|
+
|
|
255
|
+
delay = [base_delay * (2**(attempt - 1)), max_delay].min
|
|
190
256
|
sleep(delay)
|
|
191
257
|
retry
|
|
192
258
|
end
|
|
193
259
|
end
|
|
194
260
|
end
|
|
195
261
|
end
|
|
196
|
-
end
|
|
262
|
+
end
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
module KeeperSecretsManager
|
|
2
|
-
VERSION = '17.0
|
|
3
|
-
end
|
|
2
|
+
VERSION = '17.1.0'.freeze
|
|
3
|
+
end
|
|
@@ -24,15 +24,15 @@ module KeeperSecretsManager
|
|
|
24
24
|
def self.new(options = {})
|
|
25
25
|
Core::SecretsManager.new(options)
|
|
26
26
|
end
|
|
27
|
-
|
|
27
|
+
|
|
28
28
|
# Convenience method to create from token
|
|
29
29
|
def self.from_token(token, options = {})
|
|
30
30
|
Core::SecretsManager.new(options.merge(token: token))
|
|
31
31
|
end
|
|
32
|
-
|
|
32
|
+
|
|
33
33
|
# Convenience method to create from config file
|
|
34
34
|
def self.from_file(filename, options = {})
|
|
35
35
|
storage = Storage::FileStorage.new(filename)
|
|
36
36
|
Core::SecretsManager.new(options.merge(config: storage))
|
|
37
37
|
end
|
|
38
|
-
end
|
|
38
|
+
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: keeper_secrets_manager
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 17.0
|
|
4
|
+
version: 17.1.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Keeper Security
|
|
8
|
-
autorequire:
|
|
8
|
+
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2025-06
|
|
11
|
+
date: 2025-11-06 00:00:00.000000000 Z
|
|
12
12
|
dependencies: []
|
|
13
13
|
description: Ruby SDK for Keeper Secrets Manager - A zero-knowledge platform for managing
|
|
14
14
|
and protecting infrastructure secrets
|
|
@@ -25,17 +25,6 @@ files:
|
|
|
25
25
|
- LICENSE
|
|
26
26
|
- README.md
|
|
27
27
|
- Rakefile
|
|
28
|
-
- examples/basic_usage.rb
|
|
29
|
-
- examples/config_string_example.rb
|
|
30
|
-
- examples/debug_secrets.rb
|
|
31
|
-
- examples/demo_list_secrets.rb
|
|
32
|
-
- examples/download_files.rb
|
|
33
|
-
- examples/flexible_records_example.rb
|
|
34
|
-
- examples/folder_hierarchy_demo.rb
|
|
35
|
-
- examples/full_demo.rb
|
|
36
|
-
- examples/my_test_standalone.rb
|
|
37
|
-
- examples/simple_test.rb
|
|
38
|
-
- examples/storage_examples.rb
|
|
39
28
|
- lib/keeper_secrets_manager.rb
|
|
40
29
|
- lib/keeper_secrets_manager/config_keys.rb
|
|
41
30
|
- lib/keeper_secrets_manager/core.rb
|
|
@@ -60,7 +49,7 @@ metadata:
|
|
|
60
49
|
homepage_uri: https://github.com/Keeper-Security/secrets-manager
|
|
61
50
|
source_code_uri: https://github.com/Keeper-Security/secrets-manager
|
|
62
51
|
changelog_uri: https://github.com/Keeper-Security/secrets-manager/blob/master/CHANGELOG.md
|
|
63
|
-
post_install_message:
|
|
52
|
+
post_install_message:
|
|
64
53
|
rdoc_options: []
|
|
65
54
|
require_paths:
|
|
66
55
|
- lib
|
|
@@ -68,15 +57,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
68
57
|
requirements:
|
|
69
58
|
- - ">="
|
|
70
59
|
- !ruby/object:Gem::Version
|
|
71
|
-
version:
|
|
60
|
+
version: 3.1.0
|
|
72
61
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
73
62
|
requirements:
|
|
74
63
|
- - ">="
|
|
75
64
|
- !ruby/object:Gem::Version
|
|
76
65
|
version: '0'
|
|
77
66
|
requirements: []
|
|
78
|
-
rubygems_version: 3.
|
|
79
|
-
signing_key:
|
|
67
|
+
rubygems_version: 3.5.22
|
|
68
|
+
signing_key:
|
|
80
69
|
specification_version: 4
|
|
81
70
|
summary: Keeper Secrets Manager SDK for Ruby
|
|
82
71
|
test_files: []
|