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.
- checksums.yaml +7 -0
- data/.rspec +3 -0
- data/.ruby-version +1 -0
- data/CHANGELOG.md +49 -0
- data/Gemfile +13 -0
- data/LICENSE +21 -0
- data/README.md +305 -0
- data/Rakefile +30 -0
- data/examples/basic_usage.rb +139 -0
- data/examples/config_string_example.rb +99 -0
- data/examples/debug_secrets.rb +84 -0
- data/examples/demo_list_secrets.rb +182 -0
- data/examples/download_files.rb +100 -0
- data/examples/flexible_records_example.rb +94 -0
- data/examples/folder_hierarchy_demo.rb +109 -0
- data/examples/full_demo.rb +176 -0
- data/examples/my_test_standalone.rb +176 -0
- data/examples/simple_test.rb +162 -0
- data/examples/storage_examples.rb +126 -0
- data/lib/keeper_secrets_manager/config_keys.rb +27 -0
- data/lib/keeper_secrets_manager/core.rb +1231 -0
- data/lib/keeper_secrets_manager/crypto.rb +348 -0
- data/lib/keeper_secrets_manager/dto/payload.rb +152 -0
- data/lib/keeper_secrets_manager/dto.rb +221 -0
- data/lib/keeper_secrets_manager/errors.rb +79 -0
- data/lib/keeper_secrets_manager/field_types.rb +152 -0
- data/lib/keeper_secrets_manager/folder_manager.rb +114 -0
- data/lib/keeper_secrets_manager/keeper_globals.rb +59 -0
- data/lib/keeper_secrets_manager/notation.rb +354 -0
- data/lib/keeper_secrets_manager/notation_enhancements.rb +67 -0
- data/lib/keeper_secrets_manager/storage.rb +254 -0
- data/lib/keeper_secrets_manager/totp.rb +140 -0
- data/lib/keeper_secrets_manager/utils.rb +196 -0
- data/lib/keeper_secrets_manager/version.rb +3 -0
- data/lib/keeper_secrets_manager.rb +38 -0
- 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,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: []
|