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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 18b1f0bf853bb2239551325dbe053cf29d7ba68d50dadba1a502eefcc175a044
4
+ data.tar.gz: 90a4d6f1bdc8dd99c9905d2a30717cef9bde0421c9778e49c1cfe2e3bdbdb59c
5
+ SHA512:
6
+ metadata.gz: 2038821bbaa4a18b683c45415a6a5f5357ac8a8f6cc9eb677b3ad1130c6a34095f63264704af55a18be8e988d8b79eac716e5ca492694c3b2197417ab38fb1c8
7
+ data.tar.gz: cf9d6aa23e02f7e0878023216c07c36f38fe8fe4ad9c69f2ffe9c8c0748f9a9aca6de03d05a51def7dc4c407dda9ec2e4448481552d756f08f853fd02ec97127
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 3.3.8
data/CHANGELOG.md ADDED
@@ -0,0 +1,139 @@
1
+ # Changelog
2
+
3
+ ## [17.2.1]
4
+
5
+ ### Security
6
+ - **KSM-1193**: `create_secret` now encrypts the record key to the application owner's public key before sending it, matching the other SDKs. Previously the record key was placed in the create payload without that wrap, while the same request carried the record data encrypted under it. All earlier published versions are affected: 17.0.3, 17.0.4, 17.1.0 and 17.2.0; 17.0.0-17.0.2 were never published to RubyGems. A record key cannot be changed after creation, so upgrading protects newly created records only; records created by an earlier version must be re-created to place them under a new key. `create_secret` now also raises an error when the application configuration has no owner public key, instead of continuing without the encryption step.
7
+
8
+ ## [17.2.0]
9
+
10
+ ### Fixed
11
+ - **KSM-1095**: `update_secret` now calls `complete_transaction` after staging the update, so changes are committed to the server rather than remaining in a staged state indefinitely; works for both `KeeperRecord` objects and plain hash inputs
12
+ - **KSM-1094**: `update_secret` no longer raises `NameError: undefined local variable 'record_uid'` when called with a `KeeperRecord` object; the revision refresh now correctly references `record.uid`
13
+ - **KSM-1096**: `download_thumbnail` no longer raises `NoMethodError` when passed a `KeeperFile` object; `KeeperFile` now exposes a `file_key` attribute and the method dispatches on type before attempting hash access
14
+ - **KSM-824**: `to_h` now always includes `custom` in the V3 API payload, even when the array is empty, matching Commander and Vault behavior
15
+ - **KSM-906**: Added IL5 region mapping (`IL5` → `il5.keepersecurity.us`) to `KEEPER_SERVERS`
16
+ - **KSM-987**: `url_safe_str_to_bytes` and `base64_to_bytes` in `Utils` now raise `Error` when passed `nil`; all Base64 decoding in `core.rb` routes through `Utils`
17
+ - **KSM-1090**: `base64` and `logger` are now declared as explicit runtime dependencies in the gemspec; Ruby 4.0 removed these from the default standard library, so any clean install on Ruby 4.0+ previously raised `LoadError` on `require 'keeper_secrets_manager'`. Also removed a dead `require 'ostruct'` from `dto.rb`.
18
+ - **KSM-1070**: Fixed silent AES-CBC fallback in `decrypt_aes_gcm`: an AES-GCM authentication-tag failure now raises `DecryptionError` immediately rather than retrying decryption as AES-CBC; tampered or wrong-key ciphertext previously could produce output without any error.
19
+ - **KSM-685**: `CreateOptions.subfolder_uid` parameter is now correctly sent to the API when creating records
20
+ - **KSM-686**: Disaster recovery caching with `CachingPostFunction` is now implemented to match other SDKs; API response caching works for both `get_secret` and `get_folders` endpoints, and unused `@cache` and `@cache_expiry` instance variables are removed from `SecretsManager`
21
+ - **KSM-696**: Config storage file is now written with `0600` permissions (owner read/write only), preventing other local users from reading stored credentials
22
+ - **KSM-734**: Fixed notation lookup to deduplicate record shortcuts via `uniq { |r| r.uid }` before the ambiguity check; preserves genuine duplicate-title detection while preventing false ambiguity errors when both an original record and its shortcut are in scope
23
+ - **KSM-1088**: `delete_secret` and `delete_folder` now log an error for each record/folder whose `responseCode` is not `"ok"`, surfacing partial-failure details that were previously silently discarded
24
+ - **KSM-1091**: Invalid `proxy_url` now raises `ArgumentError` at initialization with a descriptive message; a URL with no host previously silently bypassed the proxy, and a fully malformed URL raised `NetworkError` at request time instead
25
+ - **KSM-1098**: `get_value`, `get_totp_code`, and `download_file` in `notation_enhancements` no longer raise `NoMethodError` when passed `nil`, an empty string, or a non-String value; `parse` itself now checks `is_a?(String)` before `empty?` to avoid `NoMethodError` for non-String inputs. `notation_enhancements` is now properly required from the main entry point.
26
+ - Fixed example files to use correct SDK APIs: `09_totp.rb` corrects `Totp` to `TOTP` and `generate()` to `generate_code()`; `01_quick_start.rb` uses the `secret.login` dynamic getter instead of hash access; `10_custom_caching.rb` uses `Utils.bytes_to_base64`
27
+ - Fixed badly anchored regular expression in `test/integration/test_totp.rb` that could cause false positives in test validation
28
+
29
+ ### Added
30
+ - **KSM-906**: `SecretsManager` now accepts the 4-part IL5 one-time token `IL5:clientKey:serverPublicKeyId:serverPublicKey`, registering the supplied EC P-256 server public key (keyId 20, outside the built-in 1–18 table) for ECIES transmission-key wrapping, and persists it as `serverPublicKey` in config so it survives restarts. Adds `server_public_key` / `server_public_key_id` constructor options (precedence: programmatic > token > config), a new `ConfigKeys::KEY_SERVER_PUBLIC_KEY`, malformed-OTT validation, and an actionable error when the backend rejects a configured custom key. Non-IL5 tokens are unchanged.
31
+ - **KSM-1013**: `KeeperRecordLink` (via `KeeperRecord#get_links`) wraps each raw `links` entry with never-raising typed accessors: permission booleans with an `allowedSettings` fallback (top-level wins), AES-256-GCM `get_decrypted_data`/`get_link_data`, and `meta`/`ai_settings`/`jit_settings` settings accessors. Adds a `request_links:` keyword to `get_secrets`. Purely additive; the raw `record.links` list is unchanged.
32
+ - **KSM-883**: On HTTP 403 `{"error":"throttled"}`, `post_query` now retries up to 5 times with exponentially increasing delays (11s, 22s, 44s, 88s, 176s) plus 0–25% jitter (one-sided), honoring `retry_after` from the response when present, and raises `ThrottledError` once retries are exhausted. Replaces the previous fixed 60-second sleep with no backoff, jitter, or retry cap.
33
+ - **KSM-1102**: Added `save(record, transaction_type: nil, links_to_remove: nil)` and `save_with_options(record, update_options)`: non-finalizing update aliases that use the stored record key without re-fetching and do not call `complete_transaction`
34
+ - **KSM-1101**: Added `inflate_field_value(uids, replace_fields)` and `get_inflate_ref_types(field_type)` for field-reference resolution; `addressRef` resolves to address fields, `cardRef` resolves to paymentCard/text/pinCode/address fields with recursive inflate
35
+ - **KSM-1100**: Added `get_notation_results` and `try_get_notation_results`: list-returning notation lookup that always returns `Array[String]`, returns all field values by default (no first-element shortcut), and JSON-serializes complex values
36
+ - **KSM-1099**: Added `create_secret_with_options(create_options, record_data, folders: nil)`: explicit options-based creation that accepts a pre-fetched folders list to avoid an extra `get_folders` network call; `create_secret` is unchanged
37
+ - **KSM-743**: Added transmission public key #18 for Gov Cloud Dev environment support
38
+ - **KSM-687**: Added DTO fields and PAM transaction support for complete SDK parity:
39
+ - `links` field to `KeeperRecord` for linked records support
40
+ - `is_editable` field to `KeeperRecord` to check edit permissions
41
+ - `inner_folder_uid` field to `KeeperRecord` for folder location tracking
42
+ - `thumbnail_url` and `last_modified` fields to `KeeperFile`
43
+ - `UpdateOptions` class with `transaction_type` and `links_to_remove` support
44
+ - `update_secret_with_options` method to support removing file links
45
+ - `request_links` option to `QueryOptions` for fetching linked records
46
+ - `download_thumbnail` method for downloading file thumbnails
47
+ - `expires_on` field to `SecretsManagerResponse`
48
+ - `complete_transaction(record_uid, rollback: false)` method for PAM rotation workflows
49
+ - `CompleteTransactionPayload` DTO class for transaction completion
50
+ - **KSM-692**: HTTP proxy support for enterprise environments:
51
+ - `proxy_url` initialization parameter for explicit proxy configuration
52
+ - `HTTPS_PROXY` / `https_proxy` environment variable support (automatic detection)
53
+ - Authenticated proxy support (username:password in URL)
54
+ - Proxy applies to all HTTP operations (API calls, file downloads, file uploads)
55
+ - **KSM-694**: Convenience methods for improved developer experience:
56
+ - `upload_file_from_path(owner_record_uid, file_path, file_title: nil)`: upload files directly from disk
57
+ - `try_get_notation(notation_uri)`: error-safe notation access (returns empty array on error)
58
+ - `KeeperSecretsManager.from_config(config_base64, options = {})`: convenience method for initializing from a base64 config string; complements `from_token()` and `from_file()` and provides parity with the .NET SDK's `GetVaultConfigFromConfigString()` pattern
59
+ - `KeeperSecretsManager::CachingPostFunction`: built-in disaster recovery caching
60
+ - `KeeperSecretsManager::Cache`: file-based cache management (save, load, clear); location configurable via `KSM_CACHE_DIR` environment variable
61
+ - Development console script (`bin/console`) for interactive SDK exploration using Pry REPL
62
+
63
+ ### Changed
64
+ - Documentation: Added Ruby SDK to root repository SDK comparison table
65
+ - **Test Coverage:**
66
+ - Added 5 new integration test files (test_pam_rotation.rb, test_proxy.rb, test_pam_linked_records.rb, test_caching.rb)
67
+ - Added 17 unit tests for caching functionality and 27 for new features (CompleteTransactionPayload, QueryOptions, proxy configuration, convenience methods)
68
+ - Enhanced test_file_operations.rb with thumbnail download and file link removal tests
69
+ - **Mock Infrastructure:** Implemented proper AES-256-GCM encryption in `mock_helper.rb`:
70
+ - Records now use proper AES-GCM encryption (was Base64 only)
71
+ - Folders use correct AES-CBC encryption for data
72
+ - Added transmission key encryption/decryption
73
+ - Added mock endpoints for transaction completion (finalize_secret_update, rollback_secret_update)
74
+ - Enabled complete offline testing without config.base64
75
+ - **Example Files:**
76
+ - Added `11_pam_linked_records.rb`: PAM resources with linked credentials and transaction workflow
77
+ - Added `12_proxy_usage.rb`: HTTP proxy configuration examples
78
+ - Updated `06_files.rb`: added `upload_file_from_path` convenience method example
79
+ - Updated `08_notation.rb`: added `try_get_notation` error-safe notation example
80
+ - Removed emojis from all example files for professional appearance
81
+ - **Dependencies:** Added base32 gem to test dependencies for TOTP support
82
+
83
+ ## [17.1.0]
84
+
85
+ ### Changed
86
+ - **BREAKING**: Minimum Ruby version increased to 3.1.0 (from 2.6.0)
87
+ - Users on older Ruby versions should upgrade or pin to keeper_secrets_manager <= 17.0.4
88
+
89
+ ### Fixed
90
+ - ECC key generation now correctly returns 32-byte raw private keys (was returning 121-byte DER format)
91
+ - Client version now dynamically uses VERSION constant instead of hardcoded value
92
+ - Fixed Tests
93
+ - `update_secret` now correctly encrypts record data before sending to server
94
+ - `update_secret` now calls `finalize_secret_update` endpoint to persist changes
95
+ - Local record's revision number is automatically refreshed after successful update
96
+ - Consecutive updates on the same record object now work without manual refetching
97
+ - `download_file` now properly respects SSL certificate verification settings and disables CRL checking
98
+ - `upload_file` now uses correct `add_file` endpoint and includes required `ownerRecordRevision` in payload
99
+ - `create_folder` now properly encrypts folder key with AES-CBC and sets correct parent_uid (nil for root-level folders)
100
+ - Fixed AES-CBC encryption to not double-pad data (OpenSSL handles padding automatically)
101
+
102
+ ## [17.0.4]
103
+
104
+ ### Changed
105
+ - Maintenance release with internal improvements
106
+
107
+ ## [17.0.3]
108
+
109
+ ### Changed
110
+ - Cleaned up directory structure
111
+ - Removed development and debug files from distribution
112
+
113
+ ## [17.0.2]
114
+
115
+ ### Security
116
+ - Updated all examples to use environment variables or placeholders
117
+
118
+ ## [17.0.1]
119
+
120
+ ### Fixed
121
+ - Added missing files to gem package (folder_manager, notation_enhancements, totp)
122
+
123
+ ## [17.0.0]
124
+
125
+ ### Added
126
+ - Initial release of Keeper Secrets Manager Ruby SDK
127
+ - Ruby 2.6+ compatibility
128
+
129
+ ### Security
130
+ - Zero-knowledge encryption using AES-GCM
131
+ - Secure key management
132
+ - SSL certificate verification
133
+
134
+ ### Notes
135
+ - Version 17.0.0 to align with other Keeper SDKs
136
+ - No runtime dependencies (base32 is optional)
137
+
138
+ [17.2.1]: https://github.com/Keeper-Security/secrets-manager/compare/ruby-sdk-v17.2.0...ruby-sdk-v17.2.1
139
+ [17.2.0]: https://github.com/Keeper-Security/secrets-manager/compare/ruby-sdk-v17.1.0...ruby-sdk-v17.2.0
data/Gemfile ADDED
@@ -0,0 +1,16 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify gem dependencies in keeper_secrets_manager.gemspec
4
+ gemspec
5
+
6
+ group :development, :test do
7
+ gem 'base32', '~> 0.3' # Required for TOTP support in tests
8
+ gem 'dotenv', '~> 2.8'
9
+ gem 'pry', '~> 0.14'
10
+ gem 'rake', '~> 13.0'
11
+ gem 'rspec', '~> 3.12'
12
+ gem 'rubocop', '~> 1.12.0'
13
+ gem 'simplecov', '~> 0.22'
14
+ gem 'webmock', '~> 3.18'
15
+ gem 'yard', '~> 0.9'
16
+ end
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Keeper Security, Inc.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,113 @@
1
+ ## Keeper Secrets Manager Ruby SDK
2
+
3
+ For more information see our official documentation page https://docs.keeper.io/secrets-manager/secrets-manager/developer-sdk-library/ruby-sdk
4
+
5
+ # Change Log
6
+
7
+ ## 17.2.1
8
+ - KSM-1193 - Security fix: `create_secret` now encrypts the record key to the application owner's public key before sending it, matching the other SDKs. Earlier versions placed the record key in the create payload without that wrap, while the same request carried the record data encrypted under it. All previously published versions are affected (17.0.3, 17.0.4, 17.1.0, 17.2.0). A record key cannot be changed after creation, so upgrading protects newly created records only; records created by an earlier version must be re-created to place them under a new key. `create_secret` now raises an error if the application configuration has no `appOwnerPublicKey`. This key is only persisted during binding, so an already-bound configuration cannot acquire it after the fact; re-bind with a fresh one-time token, or set the `KSM_APPOWNERPUBLICKEY` environment variable when using the read-only `EnvironmentStorage`.
9
+
10
+ ## 17.2.0
11
+ - KSM-685 - Fixed `CreateOptions.subfolder_uid` parameter API transmission
12
+ - KSM-686 - Implemented disaster recovery caching with `CachingPostFunction`
13
+ - KSM-687 - Added missing DTO fields for complete SDK parity (links, is_editable, inner_folder_uid, thumbnail_url, last_modified, expires_on)
14
+ - KSM-1013 - Added typed linked-credential accessors (`KeeperRecordLink` via `record.get_links`) and a `request_links:` option on `get_secrets`
15
+ - KSM-692 - Added HTTP proxy support for enterprise environments
16
+ - KSM-694 - Added convenience methods (`upload_file_from_path`, `try_get_notation`)
17
+ - KSM-696 - Fixed file permissions for Ruby SDK config files
18
+ - KSM-697 - Comprehensive unit test coverage improvements (+358 tests, 63.3% coverage)
19
+ - KSM-734 - Fixed notation lookup to handle duplicate UIDs from record shortcuts
20
+ - KSM-743 - Added transmission public key #18 for Gov Cloud Dev environment support
21
+ - Added `from_config()` convenience method for base64 config initialization
22
+ - Added `update_secret_with_options()` method for removing file links
23
+ - Added `download_thumbnail()` method for file thumbnails
24
+ - Added development console (`bin/console`) for interactive SDK exploration
25
+ - Fixed example files to use correct SDK APIs
26
+ - Improved mock infrastructure with proper AES-256-GCM encryption
27
+
28
+ ## 17.1.0
29
+ - **BREAKING**: Minimum Ruby version increased to 3.1.0 (from 2.6.0)
30
+ - Fixed ECC key generation to return 32-byte raw private keys
31
+ - Fixed `update_secret` to correctly encrypt and persist changes
32
+ - Fixed `download_file` SSL certificate verification
33
+ - Fixed `upload_file` to use correct endpoint
34
+ - Fixed `create_folder` encryption and parent_uid handling
35
+
36
+ For full version history, see [CHANGELOG.md](CHANGELOG.md)
37
+
38
+ # Quick Start
39
+
40
+ ## Installation
41
+
42
+ ```bash
43
+ gem install keeper_secrets_manager
44
+ ```
45
+
46
+ ## Basic Usage
47
+
48
+ ```ruby
49
+ require 'keeper_secrets_manager'
50
+
51
+ # Initialize from config file
52
+ secrets_manager = KeeperSecretsManager.from_file('keeper_config.json')
53
+
54
+ # Get all secrets
55
+ records = secrets_manager.get_secrets
56
+
57
+ # Access secret fields
58
+ record = records.first
59
+ puts "Password: #{record.password}"
60
+ ```
61
+
62
+ ## Linked Credentials (PAM)
63
+
64
+ PAM records can carry linked credentials. Request them with `request_links: true`, then use the typed `KeeperRecordLink` accessors returned by `record.get_links` (the raw entries remain available on `record.links`):
65
+
66
+ ```ruby
67
+ records = secrets_manager.get_secrets(request_links: true)
68
+
69
+ records.each do |record|
70
+ record.get_links.each do |link|
71
+ puts "-> #{link.record_uid} (path: #{link.path.inspect})"
72
+
73
+ # Permission booleans read allowedSettings when nested (e.g. on "meta" links)
74
+ puts " rotation allowed: #{link.allows_rotation?}"
75
+ puts " admin user: #{link.admin_user?}"
76
+
77
+ # Encrypted ai_settings / jit_settings decrypt with the owning record's key
78
+ ai = link.get_ai_settings_data(record.record_key)
79
+ puts " ai settings: #{ai.inspect}" if ai
80
+ end
81
+ end
82
+ ```
83
+
84
+ Accessors never raise — decode or decryption failures return `nil`/`false`.
85
+
86
+ ## Proxy Support
87
+
88
+ For enterprise environments behind HTTP proxies:
89
+
90
+ ```ruby
91
+ # Method 1: Explicit proxy_url parameter
92
+ secrets_manager = KeeperSecretsManager.from_file(
93
+ 'keeper_config.json',
94
+ proxy_url: 'http://proxy.company.com:8080'
95
+ )
96
+
97
+ # Method 2: Authenticated proxy
98
+ secrets_manager = KeeperSecretsManager.from_file(
99
+ 'keeper_config.json',
100
+ proxy_url: 'http://username:password@proxy.company.com:8080'
101
+ )
102
+
103
+ # Method 3: HTTPS_PROXY environment variable (recommended)
104
+ # export HTTPS_PROXY=http://proxy.company.com:8080
105
+ secrets_manager = KeeperSecretsManager.from_file('keeper_config.json')
106
+ # Proxy auto-detected from environment
107
+ ```
108
+
109
+ See `examples/ruby/12_proxy_usage.rb` for complete examples.
110
+
111
+ # Documentation
112
+
113
+ For complete documentation, see: https://docs.keeper.io/secrets-manager/secrets-manager/developer-sdk-library/ruby-sdk
data/Rakefile ADDED
@@ -0,0 +1,30 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'rspec/core/rake_task'
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task default: :spec
7
+
8
+ desc 'Run unit tests only'
9
+ RSpec::Core::RakeTask.new(:unit) do |t|
10
+ t.pattern = 'spec/**/unit/*_spec.rb'
11
+ end
12
+
13
+ desc 'Run integration tests only'
14
+ RSpec::Core::RakeTask.new(:integration) do |t|
15
+ t.pattern = 'spec/**/integration/*_spec.rb'
16
+ end
17
+
18
+ desc 'Run all tests with coverage'
19
+ task :coverage do
20
+ ENV['COVERAGE'] = 'true'
21
+ Rake::Task['spec'].invoke
22
+ end
23
+
24
+ desc 'Run RuboCop'
25
+ task :rubocop do
26
+ sh 'bundle exec rubocop'
27
+ end
28
+
29
+ desc 'Run all quality checks'
30
+ task ci: %i[rubocop spec]
data/bin/console ADDED
@@ -0,0 +1,47 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Interactive console for Keeper Secrets Manager Ruby SDK development
5
+
6
+ require 'bundler/setup'
7
+ require 'pry'
8
+
9
+ # Load the SDK from local source
10
+ require_relative '../lib/keeper_secrets_manager'
11
+
12
+ # Load environment variables if .env exists
13
+ begin
14
+ require 'dotenv/load'
15
+ puts "✓ Loaded environment variables from .env"
16
+ rescue LoadError
17
+ # dotenv not available, skip
18
+ end
19
+
20
+ # Helper message
21
+ puts "Keeper Secrets Manager Ruby SDK Console"
22
+ puts "=" * 60
23
+ puts "SDK loaded from: #{File.expand_path('../lib/keeper_secrets_manager.rb', __dir__)}"
24
+ puts "Version: #{KeeperSecretsManager::VERSION}"
25
+ puts
26
+
27
+ if ENV['KSM_CONFIG']
28
+ puts "Quick start:"
29
+ puts " storage = KeeperSecretsManager::Storage::InMemoryStorage.new(ENV['KSM_CONFIG'])"
30
+ puts " sm = KeeperSecretsManager.new(config: storage)"
31
+ puts " secrets = sm.get_secrets"
32
+ puts
33
+ puts "With caching:"
34
+ puts " sm = KeeperSecretsManager.new(config: storage, custom_post_function: KeeperSecretsManager::CachingPostFunction)"
35
+ puts
36
+ else
37
+ puts "No KSM_CONFIG found in environment"
38
+ puts "Set it in .env or export it before running"
39
+ puts
40
+ end
41
+
42
+ puts "Type 'ls KeeperSecretsManager' to explore the SDK"
43
+ puts "=" * 60
44
+ puts
45
+
46
+ # Start Pry REPL
47
+ Pry.start
@@ -0,0 +1,36 @@
1
+ require_relative 'lib/keeper_secrets_manager/version'
2
+
3
+ Gem::Specification.new do |spec|
4
+ spec.name = 'keeper_secrets_manager'
5
+ spec.version = KeeperSecretsManager::VERSION
6
+ spec.authors = ['Keeper Security']
7
+ spec.email = ['sm@keepersecurity.com']
8
+
9
+ spec.summary = 'Keeper Secrets Manager SDK for Ruby'
10
+ spec.description = 'Ruby SDK for Keeper Secrets Manager - A zero-knowledge platform for managing and protecting infrastructure secrets'
11
+ spec.homepage = 'https://github.com/Keeper-Security/secrets-manager'
12
+ spec.license = 'MIT'
13
+ spec.required_ruby_version = '>= 3.1.0'
14
+
15
+ spec.metadata['allowed_push_host'] = 'https://rubygems.org'
16
+ spec.metadata['homepage_uri'] = spec.homepage
17
+ spec.metadata['source_code_uri'] = 'https://github.com/Keeper-Security/secrets-manager'
18
+ spec.metadata['changelog_uri'] = 'https://github.com/Keeper-Security/secrets-manager/blob/master/sdk/ruby/CHANGELOG.md'
19
+
20
+ # Specify which files should be added to the gem when it is released.
21
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
22
+ `git ls-files -z`.split("\x0").reject do |f|
23
+ (f == __FILE__) || f.match(%r{\A(?:(?:test|spec|features)/|\.(?:git|travis|circleci)|appveyor)})
24
+ end
25
+ end
26
+ spec.bindir = 'exe'
27
+ spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
28
+ spec.require_paths = ['lib']
29
+
30
+ # base64 and logger were default gems through Ruby 3.x; Ruby 4.0 unbundled them.
31
+ # Both are maintained by the Ruby core team — declaring them makes the implicit explicit.
32
+ spec.add_runtime_dependency 'base64'
33
+ spec.add_runtime_dependency 'logger'
34
+
35
+ # All development dependencies are in Gemfile
36
+ end
@@ -0,0 +1,139 @@
1
+ require 'fileutils'
2
+
3
+ module KeeperSecretsManager
4
+ # File-based caching for disaster recovery
5
+ # Stores encrypted API responses to allow offline access when network is unavailable
6
+ class Cache
7
+ # Default cache file location - can be overridden with KSM_CACHE_DIR environment variable
8
+ def self.cache_file_path
9
+ cache_dir = ENV['KSM_CACHE_DIR'] || '.'
10
+ File.join(cache_dir, 'ksm_cache.bin')
11
+ end
12
+
13
+ # Save encrypted cache data (transmission key + encrypted response)
14
+ def self.save_cache(data)
15
+ File.open(cache_file_path, 'wb') do |file|
16
+ file.write(data)
17
+ end
18
+ rescue StandardError => e
19
+ # Silently fail on cache write errors (don't break the app)
20
+ warn "Failed to write cache: #{e.message}" if ENV['KSM_DEBUG']
21
+ end
22
+
23
+ # Load encrypted cache data
24
+ def self.get_cached_data
25
+ return nil unless File.exist?(cache_file_path)
26
+
27
+ File.open(cache_file_path, 'rb', &:read)
28
+ rescue StandardError => e
29
+ # Silently fail on cache read errors
30
+ warn "Failed to read cache: #{e.message}" if ENV['KSM_DEBUG']
31
+ nil
32
+ end
33
+
34
+ # Remove cache file
35
+ def self.clear_cache
36
+ File.delete(cache_file_path) if File.exist?(cache_file_path)
37
+ rescue StandardError => e
38
+ warn "Failed to delete cache: #{e.message}" if ENV['KSM_DEBUG']
39
+ end
40
+
41
+ # Check if cache file exists
42
+ def self.cache_exists?
43
+ File.exist?(cache_file_path)
44
+ end
45
+ end
46
+
47
+ # Caching post function for disaster recovery
48
+ # Wraps the normal post_function to save responses and fall back to cache on network failure
49
+ # Usage: KeeperSecretsManager.new(config: storage, custom_post_function: KeeperSecretsManager::CachingPostFunction)
50
+ module CachingPostFunction
51
+ # Post function that caches successful responses and falls back to cache on failure
52
+ # This matches the pattern used in Python, JavaScript, Java, and .NET SDKs
53
+ #
54
+ # @param url [String] The API endpoint URL
55
+ # @param transmission_key [Dto::TransmissionKey] The transmission key
56
+ # @param encrypted_payload [Dto::EncryptedPayload] The encrypted payload with signature
57
+ # @param verify_ssl_certs [Boolean] Whether to verify SSL certificates
58
+ # @return [Dto::KSMHttpResponse] Response object
59
+ def self.call(url, transmission_key, encrypted_payload, verify_ssl_certs = true)
60
+ # Try network request first
61
+ begin
62
+ # Call the static post_function
63
+ response = make_http_request(url, transmission_key, encrypted_payload, verify_ssl_certs)
64
+
65
+ # On success, save to cache (transmission key + encrypted response body)
66
+ if response.success? && response.data
67
+ cache_data = transmission_key.key + response.data
68
+ Cache.save_cache(cache_data)
69
+ end
70
+
71
+ response
72
+ rescue StandardError => e
73
+ # Network failed - try to load from cache
74
+ cached_data = Cache.get_cached_data
75
+
76
+ if cached_data && cached_data.bytesize > 32
77
+ # Extract cached transmission key and response data
78
+ # First 32 bytes are the transmission key, rest is encrypted response
79
+ cached_transmission_key = cached_data[0...32]
80
+ cached_response_data = cached_data[32..-1]
81
+
82
+ # Update the transmission key to match cached version
83
+ transmission_key.key = cached_transmission_key
84
+
85
+ # Return cached response as if it came from network
86
+ Dto::KSMHttpResponse.new(
87
+ status_code: 200,
88
+ data: cached_response_data
89
+ )
90
+ else
91
+ # No cache available - re-raise the original error
92
+ raise e
93
+ end
94
+ end
95
+ end
96
+
97
+ # Make HTTP request - extracted to be testable
98
+ # This duplicates some logic from Core::SecretsManager#post_function
99
+ # because that method is an instance method
100
+ def self.make_http_request(url, transmission_key, encrypted_payload, verify_ssl_certs)
101
+ require 'net/http'
102
+ require 'uri'
103
+
104
+ uri = URI(url)
105
+
106
+ request = Net::HTTP::Post.new(uri)
107
+ request['Content-Type'] = 'application/octet-stream'
108
+ request['PublicKeyId'] = transmission_key.public_key_id.to_s
109
+ request['TransmissionKey'] = Utils.bytes_to_base64(transmission_key.encrypted_key)
110
+ request['Authorization'] = "Signature #{Utils.bytes_to_base64(encrypted_payload.signature)}"
111
+ request['Content-Length'] = encrypted_payload.encrypted_payload.bytesize.to_s
112
+ request.body = encrypted_payload.encrypted_payload
113
+
114
+ http = Net::HTTP.new(uri.host, uri.port)
115
+ http.use_ssl = true
116
+
117
+ if verify_ssl_certs
118
+ http.verify_mode = OpenSSL::SSL::VERIFY_PEER
119
+
120
+ # Set up certificate store with system defaults
121
+ store = OpenSSL::X509::Store.new
122
+ store.set_default_paths
123
+ http.cert_store = store
124
+ else
125
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE
126
+ end
127
+
128
+ response = http.request(request)
129
+
130
+ Dto::KSMHttpResponse.new(
131
+ status_code: response.code.to_i,
132
+ data: response.body,
133
+ http_response: response
134
+ )
135
+ rescue StandardError => e
136
+ raise NetworkError, "HTTP request failed: #{e.message}"
137
+ end
138
+ end
139
+ end
@@ -0,0 +1,29 @@
1
+ module KeeperSecretsManager
2
+ module ConfigKeys
3
+ # Configuration key constants (matching other SDKs)
4
+ KEY_URL = 'url'.freeze
5
+ KEY_CLIENT_ID = 'clientId'.freeze
6
+ KEY_CLIENT_KEY = 'clientKey'.freeze
7
+ KEY_HOSTNAME = 'hostname'.freeze
8
+ KEY_SERVER_PUBLIC_KEY_ID = 'serverPublicKeyId'.freeze
9
+ KEY_SERVER_PUBLIC_KEY = 'serverPublicKey'.freeze
10
+ KEY_PRIVATE_KEY = 'privateKey'.freeze
11
+ KEY_APP_KEY = 'appKey'.freeze
12
+ KEY_OWNER_PUBLIC_KEY = 'appOwnerPublicKey'.freeze
13
+ KEY_APP_UID = 'appUid'.freeze
14
+
15
+ # All valid keys
16
+ ALL_KEYS = [
17
+ KEY_URL,
18
+ KEY_CLIENT_ID,
19
+ KEY_CLIENT_KEY,
20
+ KEY_HOSTNAME,
21
+ KEY_SERVER_PUBLIC_KEY_ID,
22
+ KEY_SERVER_PUBLIC_KEY,
23
+ KEY_PRIVATE_KEY,
24
+ KEY_APP_KEY,
25
+ KEY_OWNER_PUBLIC_KEY,
26
+ KEY_APP_UID
27
+ ].freeze
28
+ end
29
+ end