keeper_secrets_manager 17.0.3

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 (36) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +3 -0
  3. data/.ruby-version +1 -0
  4. data/CHANGELOG.md +49 -0
  5. data/Gemfile +13 -0
  6. data/LICENSE +21 -0
  7. data/README.md +305 -0
  8. data/Rakefile +30 -0
  9. data/examples/basic_usage.rb +139 -0
  10. data/examples/config_string_example.rb +99 -0
  11. data/examples/debug_secrets.rb +84 -0
  12. data/examples/demo_list_secrets.rb +182 -0
  13. data/examples/download_files.rb +100 -0
  14. data/examples/flexible_records_example.rb +94 -0
  15. data/examples/folder_hierarchy_demo.rb +109 -0
  16. data/examples/full_demo.rb +176 -0
  17. data/examples/my_test_standalone.rb +176 -0
  18. data/examples/simple_test.rb +162 -0
  19. data/examples/storage_examples.rb +126 -0
  20. data/lib/keeper_secrets_manager/config_keys.rb +27 -0
  21. data/lib/keeper_secrets_manager/core.rb +1231 -0
  22. data/lib/keeper_secrets_manager/crypto.rb +348 -0
  23. data/lib/keeper_secrets_manager/dto/payload.rb +152 -0
  24. data/lib/keeper_secrets_manager/dto.rb +221 -0
  25. data/lib/keeper_secrets_manager/errors.rb +79 -0
  26. data/lib/keeper_secrets_manager/field_types.rb +152 -0
  27. data/lib/keeper_secrets_manager/folder_manager.rb +114 -0
  28. data/lib/keeper_secrets_manager/keeper_globals.rb +59 -0
  29. data/lib/keeper_secrets_manager/notation.rb +354 -0
  30. data/lib/keeper_secrets_manager/notation_enhancements.rb +67 -0
  31. data/lib/keeper_secrets_manager/storage.rb +254 -0
  32. data/lib/keeper_secrets_manager/totp.rb +140 -0
  33. data/lib/keeper_secrets_manager/utils.rb +196 -0
  34. data/lib/keeper_secrets_manager/version.rb +3 -0
  35. data/lib/keeper_secrets_manager.rb +38 -0
  36. metadata +82 -0
@@ -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,196 @@
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
+ Base64.strict_decode64(str)
39
+ rescue ArgumentError => e
40
+ raise Error, "Invalid base64: #{e.message}"
41
+ end
42
+
43
+ # URL-safe base64 encode (with padding)
44
+ def url_safe_str_to_bytes(str)
45
+ # Add padding if needed
46
+ str += '=' * (4 - str.length % 4) if str.length % 4 != 0
47
+ Base64.urlsafe_decode64(str)
48
+ end
49
+
50
+ # URL-safe base64 decode (without padding)
51
+ def bytes_to_url_safe_str(bytes)
52
+ Base64.urlsafe_encode64(bytes).delete('=')
53
+ end
54
+
55
+ # Generate random bytes
56
+ def generate_random_bytes(length)
57
+ SecureRandom.random_bytes(length)
58
+ end
59
+
60
+ # Generate UID (16 random bytes)
61
+ def generate_uid
62
+ bytes_to_url_safe_str(generate_random_bytes(16))
63
+ end
64
+
65
+ # Generate UID bytes
66
+ def generate_uid_bytes
67
+ generate_random_bytes(16)
68
+ end
69
+
70
+ # Get current time in milliseconds
71
+ def now_milliseconds
72
+ (Time.now.to_f * 1000).to_i
73
+ end
74
+
75
+ # Convert string to boolean
76
+ def strtobool(val)
77
+ return val if val.is_a?(TrueClass) || val.is_a?(FalseClass)
78
+
79
+ val_str = val.to_s.downcase.strip
80
+ case val_str
81
+ when 'true', '1', 'yes', 'y', 'on'
82
+ true
83
+ when 'false', '0', 'no', 'n', 'off', ''
84
+ false
85
+ else
86
+ raise ArgumentError, "Invalid boolean value: #{val}"
87
+ end
88
+ end
89
+
90
+ # Check if string is blank
91
+ def blank?(str)
92
+ str.nil? || str.strip.empty?
93
+ end
94
+
95
+ # Deep merge hashes
96
+ def deep_merge(hash1, hash2)
97
+ hash1.merge(hash2) do |key, old_val, new_val|
98
+ if old_val.is_a?(Hash) && new_val.is_a?(Hash)
99
+ deep_merge(old_val, new_val)
100
+ else
101
+ new_val
102
+ end
103
+ end
104
+ end
105
+
106
+ # Convert camelCase to snake_case
107
+ def camel_to_snake(str)
108
+ str.gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
109
+ .gsub(/([a-z\d])([A-Z])/, '\1_\2')
110
+ .downcase
111
+ end
112
+
113
+ # Convert snake_case to camelCase
114
+ def snake_to_camel(str, capitalize_first = false)
115
+ result = str.split('_').map.with_index do |word, i|
116
+ i == 0 && !capitalize_first ? word : word.capitalize
117
+ end.join
118
+ result
119
+ end
120
+
121
+ # Safe integer conversion
122
+ def to_int(val, default = nil)
123
+ Integer(val)
124
+ rescue ArgumentError, TypeError
125
+ default
126
+ end
127
+
128
+ # URL join
129
+ def url_join(*parts)
130
+ parts.map { |part| part.to_s.gsub(%r{^/+|/+$}, '') }
131
+ .reject(&:empty?)
132
+ .join('/')
133
+ end
134
+
135
+ # Parse server URL from hostname
136
+ def get_server_url(hostname, use_ssl = true)
137
+ return nil if blank?(hostname)
138
+
139
+ # Remove protocol if present
140
+ hostname = hostname.sub(%r{^https?://}, '')
141
+
142
+ # Build URL
143
+ protocol = use_ssl ? 'https' : 'http'
144
+ "#{protocol}://#{hostname}"
145
+ end
146
+
147
+ # Extract region from token or hostname
148
+ def extract_region(token_or_hostname)
149
+ # Check if it's a token with region prefix
150
+ if token_or_hostname&.include?(':')
151
+ parts = token_or_hostname.split(':')
152
+ return parts[0].upcase if parts.length >= 2
153
+ end
154
+
155
+ # Check if hostname matches a known region
156
+ hostname = token_or_hostname.to_s.downcase
157
+ KeeperGlobals::KEEPER_SERVERS.each do |region, server|
158
+ return region if hostname.include?(server)
159
+ end
160
+
161
+ # Default to US
162
+ 'US'
163
+ end
164
+
165
+ # Validate UID format
166
+ def valid_uid?(uid)
167
+ return false if blank?(uid)
168
+
169
+ # UIDs are base64url encoded 16-byte values
170
+ begin
171
+ bytes = url_safe_str_to_bytes(uid)
172
+ bytes.length == 16
173
+ rescue
174
+ false
175
+ end
176
+ end
177
+
178
+ # Retry with exponential backoff
179
+ def retry_with_backoff(max_attempts: 3, base_delay: 1, max_delay: 60)
180
+ attempt = 0
181
+ begin
182
+ yield
183
+ rescue => e
184
+ attempt += 1
185
+ if attempt >= max_attempts
186
+ raise e
187
+ end
188
+
189
+ delay = [base_delay * (2 ** (attempt - 1)), max_delay].min
190
+ sleep(delay)
191
+ retry
192
+ end
193
+ end
194
+ end
195
+ end
196
+ end
@@ -0,0 +1,3 @@
1
+ module KeeperSecretsManager
2
+ VERSION = '17.0.3'.freeze
3
+ end
@@ -0,0 +1,38 @@
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/core'
12
+ require 'keeper_secrets_manager/folder_manager'
13
+
14
+ # Optional TOTP support (only load if base32 gem is available)
15
+ begin
16
+ require 'keeper_secrets_manager/totp'
17
+ rescue LoadError => e
18
+ # TOTP support not available without base32 gem
19
+ # This is optional functionality
20
+ end
21
+
22
+ module KeeperSecretsManager
23
+ # Main entry point for the SDK
24
+ def self.new(options = {})
25
+ Core::SecretsManager.new(options)
26
+ end
27
+
28
+ # Convenience method to create from token
29
+ def self.from_token(token, options = {})
30
+ Core::SecretsManager.new(options.merge(token: token))
31
+ end
32
+
33
+ # Convenience method to create from config file
34
+ def self.from_file(filename, options = {})
35
+ storage = Storage::FileStorage.new(filename)
36
+ Core::SecretsManager.new(options.merge(config: storage))
37
+ end
38
+ end
metadata ADDED
@@ -0,0 +1,82 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: keeper_secrets_manager
3
+ version: !ruby/object:Gem::Version
4
+ version: 17.0.3
5
+ platform: ruby
6
+ authors:
7
+ - Keeper Security
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2025-06-25 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: Ruby SDK for Keeper Secrets Manager - A zero-knowledge platform for managing
14
+ and protecting infrastructure secrets
15
+ email:
16
+ - sm@keepersecurity.com
17
+ executables: []
18
+ extensions: []
19
+ extra_rdoc_files: []
20
+ files:
21
+ - ".rspec"
22
+ - ".ruby-version"
23
+ - CHANGELOG.md
24
+ - Gemfile
25
+ - LICENSE
26
+ - README.md
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
+ - lib/keeper_secrets_manager.rb
40
+ - lib/keeper_secrets_manager/config_keys.rb
41
+ - lib/keeper_secrets_manager/core.rb
42
+ - lib/keeper_secrets_manager/crypto.rb
43
+ - lib/keeper_secrets_manager/dto.rb
44
+ - lib/keeper_secrets_manager/dto/payload.rb
45
+ - lib/keeper_secrets_manager/errors.rb
46
+ - lib/keeper_secrets_manager/field_types.rb
47
+ - lib/keeper_secrets_manager/folder_manager.rb
48
+ - lib/keeper_secrets_manager/keeper_globals.rb
49
+ - lib/keeper_secrets_manager/notation.rb
50
+ - lib/keeper_secrets_manager/notation_enhancements.rb
51
+ - lib/keeper_secrets_manager/storage.rb
52
+ - lib/keeper_secrets_manager/totp.rb
53
+ - lib/keeper_secrets_manager/utils.rb
54
+ - lib/keeper_secrets_manager/version.rb
55
+ homepage: https://github.com/Keeper-Security/secrets-manager
56
+ licenses:
57
+ - MIT
58
+ metadata:
59
+ allowed_push_host: https://rubygems.org
60
+ homepage_uri: https://github.com/Keeper-Security/secrets-manager
61
+ source_code_uri: https://github.com/Keeper-Security/secrets-manager
62
+ changelog_uri: https://github.com/Keeper-Security/secrets-manager/blob/master/CHANGELOG.md
63
+ post_install_message:
64
+ rdoc_options: []
65
+ require_paths:
66
+ - lib
67
+ required_ruby_version: !ruby/object:Gem::Requirement
68
+ requirements:
69
+ - - ">="
70
+ - !ruby/object:Gem::Version
71
+ version: 2.6.0
72
+ required_rubygems_version: !ruby/object:Gem::Requirement
73
+ requirements:
74
+ - - ">="
75
+ - !ruby/object:Gem::Version
76
+ version: '0'
77
+ requirements: []
78
+ rubygems_version: 3.0.3.1
79
+ signing_key:
80
+ specification_version: 4
81
+ summary: Keeper Secrets Manager SDK for Ruby
82
+ test_files: []