keeper_secrets_manager 17.2.1

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.
@@ -0,0 +1,254 @@
1
+ require 'json'
2
+ require 'base64'
3
+ require 'fileutils'
4
+
5
+ module KeeperSecretsManager
6
+ module Storage
7
+ # Base storage interface
8
+ module KeyValueStorage
9
+ def get_string(_key)
10
+ raise NotImplementedError, 'Subclass must implement get_string'
11
+ end
12
+
13
+ def save_string(_key, _value)
14
+ raise NotImplementedError, 'Subclass must implement save_string'
15
+ end
16
+
17
+ def get_bytes(key)
18
+ data = get_string(key)
19
+ return nil unless data
20
+
21
+ # Handle both standard and URL-safe base64
22
+ begin
23
+ # First try standard base64
24
+ Base64.strict_decode64(data)
25
+ rescue ArgumentError
26
+ begin
27
+ # Try URL-safe base64 with padding
28
+ padding = 4 - (data.length % 4)
29
+ padding = 0 if padding == 4
30
+ Base64.urlsafe_decode64(data + '=' * padding)
31
+ rescue StandardError => e
32
+ # Last resort - try with decode64 which is more lenient
33
+ Base64.decode64(data)
34
+ end
35
+ end
36
+ end
37
+
38
+ def save_bytes(key, value)
39
+ save_string(key, Base64.strict_encode64(value))
40
+ end
41
+
42
+ def delete(_key)
43
+ raise NotImplementedError, 'Subclass must implement delete'
44
+ end
45
+
46
+ def contains?(key)
47
+ !get_string(key).nil?
48
+ end
49
+ end
50
+
51
+ # In-memory storage implementation
52
+ class InMemoryStorage
53
+ include KeyValueStorage
54
+
55
+ def initialize(config_data = nil)
56
+ @data = {}
57
+
58
+ # Initialize from JSON string, base64 string, or hash
59
+ if config_data
60
+ parsed = case config_data
61
+ when String
62
+ # Check if it's base64 encoded
63
+ if is_base64?(config_data)
64
+ JSON.parse(Base64.decode64(config_data))
65
+ else
66
+ JSON.parse(config_data)
67
+ end
68
+ when Hash
69
+ config_data
70
+ else
71
+ {}
72
+ end
73
+
74
+ parsed.each { |k, v| @data[k.to_s] = v.to_s }
75
+ end
76
+ end
77
+
78
+ def get_string(key)
79
+ @data[key.to_s]
80
+ end
81
+
82
+ def save_string(key, value)
83
+ @data[key.to_s] = value.to_s
84
+ end
85
+
86
+ def delete(key)
87
+ @data.delete(key.to_s)
88
+ end
89
+
90
+ def to_h
91
+ @data.dup
92
+ end
93
+
94
+ def to_json(*args)
95
+ @data.to_json(*args)
96
+ end
97
+
98
+ private
99
+
100
+ def is_base64?(str)
101
+ # Check if string is valid base64
102
+ return false if str.nil? || str.empty?
103
+
104
+ # Remove whitespace
105
+ str = str.strip
106
+
107
+ # Check if length is multiple of 4 (with padding) or can be padded to multiple of 4
108
+ # Also check if it only contains base64 characters
109
+ base64_regex = %r{\A[A-Za-z0-9+/]*={0,2}\z}
110
+
111
+ str.match?(base64_regex) && (str.length % 4 == 0 || str.length % 4 == 2 || str.length % 4 == 3)
112
+ end
113
+ end
114
+
115
+ # File-based storage implementation
116
+ class FileStorage
117
+ include KeyValueStorage
118
+
119
+ def initialize(filename = 'keeper_config.json')
120
+ @filename = File.expand_path(filename)
121
+ @data = {}
122
+ load_data
123
+ end
124
+
125
+ def get_string(key)
126
+ @data[key.to_s]
127
+ end
128
+
129
+ def save_string(key, value)
130
+ @data[key.to_s] = value.to_s
131
+ save_data
132
+ end
133
+
134
+ def delete(key)
135
+ @data.delete(key.to_s)
136
+ save_data
137
+ end
138
+
139
+ private
140
+
141
+ def load_data
142
+ if File.exist?(@filename)
143
+ begin
144
+ content = File.read(@filename)
145
+ # Handle empty files
146
+ @data = if content.strip.empty?
147
+ {}
148
+ else
149
+ JSON.parse(content)
150
+ end
151
+ rescue JSON::ParserError => e
152
+ raise Error, "Failed to parse config file: #{e.message}"
153
+ end
154
+ end
155
+ end
156
+
157
+ def save_data
158
+ # Ensure directory exists
159
+ FileUtils.mkdir_p(File.dirname(@filename))
160
+
161
+ # Write atomically to avoid corruption
162
+ temp_file = "#{@filename}.tmp"
163
+ # Create temp file with secure permissions (0600)
164
+ File.open(temp_file, 'w', 0o600) do |f|
165
+ f.write(JSON.pretty_generate(@data))
166
+ end
167
+
168
+ # Move atomically
169
+ File.rename(temp_file, @filename)
170
+
171
+ # Ensure final file has restrictive permissions (owner read/write only)
172
+ File.chmod(0o600, @filename)
173
+ rescue StandardError => e
174
+ raise Error, "Failed to save config file: #{e.message}"
175
+ end
176
+ end
177
+
178
+ # Environment-based storage (read-only)
179
+ class EnvironmentStorage
180
+ include KeyValueStorage
181
+
182
+ def initialize(prefix = 'KSM_')
183
+ @prefix = prefix
184
+ end
185
+
186
+ def get_string(key)
187
+ ENV["#{@prefix}#{key.to_s.upcase}"]
188
+ end
189
+
190
+ def save_string(_key, _value)
191
+ raise Error, 'Environment storage is read-only'
192
+ end
193
+
194
+ def delete(_key)
195
+ raise Error, 'Environment storage is read-only'
196
+ end
197
+ end
198
+
199
+ # Cacheable storage wrapper
200
+ class CachingStorage
201
+ include KeyValueStorage
202
+
203
+ def initialize(base_storage, ttl_seconds = 600)
204
+ @base_storage = base_storage
205
+ @ttl_seconds = ttl_seconds
206
+ @cache = {}
207
+ @timestamps = {}
208
+ end
209
+
210
+ def get_string(key)
211
+ key_str = key.to_s
212
+
213
+ # Check cache validity
214
+ return @cache[key_str] if @cache.key?(key_str) && !expired?(key_str)
215
+
216
+ # Fetch from base storage
217
+ value = @base_storage.get_string(key)
218
+ if value
219
+ @cache[key_str] = value
220
+ @timestamps[key_str] = Time.now
221
+ end
222
+
223
+ value
224
+ end
225
+
226
+ def save_string(key, value)
227
+ key_str = key.to_s
228
+ @base_storage.save_string(key, value)
229
+ @cache[key_str] = value.to_s
230
+ @timestamps[key_str] = Time.now
231
+ end
232
+
233
+ def delete(key)
234
+ key_str = key.to_s
235
+ @base_storage.delete(key)
236
+ @cache.delete(key_str)
237
+ @timestamps.delete(key_str)
238
+ end
239
+
240
+ def clear_cache
241
+ @cache.clear
242
+ @timestamps.clear
243
+ end
244
+
245
+ private
246
+
247
+ def expired?(key)
248
+ return true unless @timestamps[key]
249
+
250
+ Time.now - @timestamps[key] > @ttl_seconds
251
+ end
252
+ end
253
+ end
254
+ end
@@ -0,0 +1,140 @@
1
+ # TOTP (Time-based One-Time Password) implementation
2
+ # Compliant with RFC 6238
3
+
4
+ require 'base32'
5
+ require 'openssl'
6
+ require 'uri'
7
+
8
+ module KeeperSecretsManager
9
+ class TOTP
10
+ ALGORITHMS = {
11
+ 'SHA1' => OpenSSL::Digest::SHA1,
12
+ 'SHA256' => OpenSSL::Digest::SHA256,
13
+ 'SHA512' => OpenSSL::Digest::SHA512
14
+ }.freeze
15
+
16
+ # Generate a TOTP code
17
+ # @param secret [String] Base32 encoded secret
18
+ # @param time [Time] Time to generate code for (default: current time)
19
+ # @param algorithm [String] Hash algorithm: SHA1, SHA256, or SHA512
20
+ # @param digits [Integer] Number of digits (6 or 8)
21
+ # @param period [Integer] Time period in seconds
22
+ # @return [String] TOTP code
23
+ def self.generate_code(secret, time: Time.now, algorithm: 'SHA1', digits: 6, period: 30)
24
+ # Validate inputs
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
+
29
+ # Decode base32 secret
30
+ key = Base32.decode(secret.upcase.tr(' ', ''))
31
+
32
+ # Calculate time counter
33
+ counter = (time.to_i / period).floor
34
+
35
+ # Convert counter to 8-byte string (big-endian)
36
+ counter_bytes = [counter].pack('Q>')
37
+
38
+ # Generate HMAC
39
+ digest = ALGORITHMS[algorithm].new
40
+ hmac = OpenSSL::HMAC.digest(digest, key, counter_bytes)
41
+
42
+ # Extract dynamic binary code
43
+ offset = hmac[-1].ord & 0x0f
44
+ code = (hmac[offset].ord & 0x7f) << 24 |
45
+ (hmac[offset + 1].ord & 0xff) << 16 |
46
+ (hmac[offset + 2].ord & 0xff) << 8 |
47
+ (hmac[offset + 3].ord & 0xff)
48
+
49
+ # Generate final OTP value
50
+ otp = code % (10**digits)
51
+
52
+ # Pad with leading zeros if necessary
53
+ otp.to_s.rjust(digits, '0')
54
+ end
55
+
56
+ # Parse TOTP URL (otpauth://totp/...)
57
+ # @param url [String] TOTP URL
58
+ # @return [Hash] Parsed components
59
+ def self.parse_url(url)
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
+
65
+ # Extract label (issuer:account or just account)
66
+ path = uri.path[1..-1] # Remove leading /
67
+ if path.include?(':')
68
+ issuer, account = path.split(':', 2)
69
+ else
70
+ account = path
71
+ issuer = nil
72
+ end
73
+
74
+ # Parse query parameters
75
+ params = URI.decode_www_form(uri.query || '').to_h
76
+
77
+ {
78
+ 'account' => URI.decode_www_form_component(account || ''),
79
+ 'issuer' => issuer ? URI.decode_www_form_component(issuer) : params['issuer'],
80
+ 'secret' => params['secret'],
81
+ 'algorithm' => params['algorithm'] || 'SHA1',
82
+ 'digits' => (params['digits'] || '6').to_i,
83
+ 'period' => (params['period'] || '30').to_i
84
+ }
85
+ end
86
+
87
+ # Generate TOTP URL
88
+ # @param account [String] Account name (e.g., email)
89
+ # @param secret [String] Base32 encoded secret
90
+ # @param issuer [String] Service name
91
+ # @param algorithm [String] Hash algorithm
92
+ # @param digits [Integer] Number of digits
93
+ # @param period [Integer] Time period
94
+ # @return [String] TOTP URL
95
+ def self.generate_url(account, secret, issuer: nil, algorithm: 'SHA1', digits: 6, period: 30)
96
+ label = issuer ? "#{issuer}:#{account}" : account
97
+
98
+ params = {
99
+ 'secret' => secret,
100
+ 'algorithm' => algorithm,
101
+ 'digits' => digits,
102
+ 'period' => period
103
+ }
104
+
105
+ params['issuer'] = issuer if issuer
106
+
107
+ query = URI.encode_www_form(params)
108
+ "otpauth://totp/#{URI.encode_www_form_component(label)}?#{query}"
109
+ end
110
+
111
+ # Validate a TOTP code
112
+ # @param secret [String] Base32 encoded secret
113
+ # @param code [String] Code to validate
114
+ # @param time [Time] Time to validate against
115
+ # @param window [Integer] Number of periods to check before/after
116
+ # @param algorithm [String] Hash algorithm
117
+ # @param digits [Integer] Number of digits
118
+ # @param period [Integer] Time period
119
+ # @return [Boolean] True if code is valid
120
+ def self.validate_code(secret, code, time: Time.now, window: 1, algorithm: 'SHA1', digits: 6, period: 30)
121
+ # Check current time and window
122
+ (-window..window).each do |offset|
123
+ test_time = time + (offset * period)
124
+ test_code = generate_code(secret, time: test_time, algorithm: algorithm, digits: digits, period: period)
125
+
126
+ return true if test_code == code
127
+ end
128
+
129
+ false
130
+ end
131
+
132
+ # Generate a random secret suitable for TOTP
133
+ # @param length [Integer] Number of bytes (default: 20 for 160 bits)
134
+ # @return [String] Base32 encoded secret
135
+ def self.generate_secret(length: 20)
136
+ random_bytes = OpenSSL::Random.random_bytes(length)
137
+ Base32.encode(random_bytes).delete('=')
138
+ end
139
+ end
140
+ end
@@ -0,0 +1,263 @@
1
+ require 'json'
2
+ require 'base64'
3
+ require 'securerandom'
4
+ require 'time'
5
+
6
+ module KeeperSecretsManager
7
+ module Utils
8
+ class << self
9
+ # Convert string to bytes
10
+ def string_to_bytes(str)
11
+ str.b
12
+ end
13
+
14
+ # Convert bytes to string
15
+ def bytes_to_string(bytes)
16
+ bytes.force_encoding('UTF-8')
17
+ end
18
+
19
+ # Convert hash/object to JSON string
20
+ def dict_to_json(obj)
21
+ JSON.generate(obj)
22
+ end
23
+
24
+ # Parse JSON string to hash
25
+ def json_to_dict(json_str)
26
+ JSON.parse(json_str)
27
+ rescue JSON::ParserError => e
28
+ raise Error, "Invalid JSON: #{e.message}"
29
+ end
30
+
31
+ # Base64 encode
32
+ def bytes_to_base64(bytes)
33
+ Base64.strict_encode64(bytes)
34
+ end
35
+
36
+ # Base64 decode
37
+ def base64_to_bytes(str)
38
+ raise Error, 'base64_to_bytes: received nil' if str.nil?
39
+ Base64.strict_decode64(str)
40
+ rescue ArgumentError => e
41
+ raise Error, "Invalid base64: #{e.message}"
42
+ end
43
+
44
+ # URL-safe base64 encode (with padding)
45
+ def url_safe_str_to_bytes(str)
46
+ raise Error, 'url_safe_str_to_bytes: received nil' if str.nil?
47
+ str += '=' * (4 - str.length % 4) if str.length % 4 != 0
48
+ Base64.urlsafe_decode64(str)
49
+ end
50
+
51
+ # URL-safe base64 decode (without padding)
52
+ def bytes_to_url_safe_str(bytes)
53
+ Base64.urlsafe_encode64(bytes).delete('=')
54
+ end
55
+
56
+ # Generate random bytes
57
+ def generate_random_bytes(length)
58
+ SecureRandom.random_bytes(length)
59
+ end
60
+
61
+ # Generate UID (16 random bytes)
62
+ def generate_uid
63
+ bytes_to_url_safe_str(generate_random_bytes(16))
64
+ end
65
+
66
+ # Generate UID bytes
67
+ def generate_uid_bytes
68
+ generate_random_bytes(16)
69
+ end
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
+
140
+ # Get current time in milliseconds
141
+ def now_milliseconds
142
+ (Time.now.to_f * 1000).to_i
143
+ end
144
+
145
+ # Convert string to boolean
146
+ def strtobool(val)
147
+ return val if val.is_a?(TrueClass) || val.is_a?(FalseClass)
148
+
149
+ val_str = val.to_s.downcase.strip
150
+ case val_str
151
+ when 'true', '1', 'yes', 'y', 'on'
152
+ true
153
+ when 'false', '0', 'no', 'n', 'off', ''
154
+ false
155
+ else
156
+ raise ArgumentError, "Invalid boolean value: #{val}"
157
+ end
158
+ end
159
+
160
+ # Check if string is blank
161
+ def blank?(str)
162
+ str.nil? || str.strip.empty?
163
+ end
164
+
165
+ # Deep merge hashes
166
+ def deep_merge(hash1, hash2)
167
+ hash1.merge(hash2) do |_key, old_val, new_val|
168
+ if old_val.is_a?(Hash) && new_val.is_a?(Hash)
169
+ deep_merge(old_val, new_val)
170
+ else
171
+ new_val
172
+ end
173
+ end
174
+ end
175
+
176
+ # Convert camelCase to snake_case
177
+ def camel_to_snake(str)
178
+ str.gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
179
+ .gsub(/([a-z\d])([A-Z])/, '\1_\2')
180
+ .downcase
181
+ end
182
+
183
+ # Convert snake_case to camelCase
184
+ def snake_to_camel(str, capitalize_first = false)
185
+ str.split('_').map.with_index do |word, i|
186
+ i == 0 && !capitalize_first ? word : word.capitalize
187
+ end.join
188
+ end
189
+
190
+ # Safe integer conversion
191
+ def to_int(val, default = nil)
192
+ Integer(val)
193
+ rescue ArgumentError, TypeError
194
+ default
195
+ end
196
+
197
+ # URL join
198
+ def url_join(*parts)
199
+ parts.map { |part| part.to_s.gsub(%r{^/+|/+$}, '') }
200
+ .reject(&:empty?)
201
+ .join('/')
202
+ end
203
+
204
+ # Parse server URL from hostname
205
+ def get_server_url(hostname, use_ssl = true)
206
+ return nil if blank?(hostname)
207
+
208
+ # Remove protocol if present
209
+ hostname = hostname.sub(%r{^https?://}, '')
210
+
211
+ # Build URL
212
+ protocol = use_ssl ? 'https' : 'http'
213
+ "#{protocol}://#{hostname}"
214
+ end
215
+
216
+ # Extract region from token or hostname
217
+ def extract_region(token_or_hostname)
218
+ # Check if it's a token with region prefix
219
+ if token_or_hostname&.include?(':')
220
+ parts = token_or_hostname.split(':')
221
+ return parts[0].upcase if parts.length >= 2
222
+ end
223
+
224
+ # Check if hostname matches a known region
225
+ hostname = token_or_hostname.to_s.downcase
226
+ KeeperGlobals::KEEPER_SERVERS.each do |region, server|
227
+ return region if hostname.include?(server)
228
+ end
229
+
230
+ # Default to US
231
+ 'US'
232
+ end
233
+
234
+ # Validate UID format
235
+ def valid_uid?(uid)
236
+ return false if blank?(uid)
237
+
238
+ # UIDs are base64url encoded 16-byte values
239
+ begin
240
+ bytes = url_safe_str_to_bytes(uid)
241
+ bytes.length == 16
242
+ rescue StandardError
243
+ false
244
+ end
245
+ end
246
+
247
+ # Retry with exponential backoff
248
+ def retry_with_backoff(max_attempts: 3, base_delay: 1, max_delay: 60)
249
+ attempt = 0
250
+ begin
251
+ yield
252
+ rescue StandardError => e
253
+ attempt += 1
254
+ raise e if attempt >= max_attempts
255
+
256
+ delay = [base_delay * (2**(attempt - 1)), max_delay].min
257
+ sleep(delay)
258
+ retry
259
+ end
260
+ end
261
+ end
262
+ end
263
+ end
@@ -0,0 +1,3 @@
1
+ module KeeperSecretsManager
2
+ VERSION = '17.2.1'.freeze
3
+ end
@@ -0,0 +1,46 @@
1
+ require 'keeper_secrets_manager/version'
2
+ require 'keeper_secrets_manager/errors'
3
+ require 'keeper_secrets_manager/config_keys'
4
+ require 'keeper_secrets_manager/keeper_globals'
5
+ require 'keeper_secrets_manager/utils'
6
+ require 'keeper_secrets_manager/crypto'
7
+ require 'keeper_secrets_manager/storage'
8
+ require 'keeper_secrets_manager/dto'
9
+ require 'keeper_secrets_manager/field_types'
10
+ require 'keeper_secrets_manager/notation'
11
+ require 'keeper_secrets_manager/notation_enhancements'
12
+ require 'keeper_secrets_manager/cache'
13
+ require 'keeper_secrets_manager/core'
14
+ require 'keeper_secrets_manager/folder_manager'
15
+
16
+ # Optional TOTP support (only load if base32 gem is available)
17
+ begin
18
+ require 'keeper_secrets_manager/totp'
19
+ rescue LoadError => e
20
+ # TOTP support not available without base32 gem
21
+ # This is optional functionality
22
+ end
23
+
24
+ module KeeperSecretsManager
25
+ # Main entry point for the SDK
26
+ def self.new(options = {})
27
+ Core::SecretsManager.new(options)
28
+ end
29
+
30
+ # Convenience method to create from token
31
+ def self.from_token(token, options = {})
32
+ Core::SecretsManager.new(options.merge(token: token))
33
+ end
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
+
41
+ # Convenience method to create from config file
42
+ def self.from_file(filename, options = {})
43
+ storage = Storage::FileStorage.new(filename)
44
+ Core::SecretsManager.new(options.merge(config: storage))
45
+ end
46
+ end