keeper_secrets_manager 17.1.0 → 17.2.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.
@@ -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
@@ -6,6 +6,7 @@ module KeeperSecretsManager
6
6
  KEY_CLIENT_KEY = 'clientKey'.freeze
7
7
  KEY_HOSTNAME = 'hostname'.freeze
8
8
  KEY_SERVER_PUBLIC_KEY_ID = 'serverPublicKeyId'.freeze
9
+ KEY_SERVER_PUBLIC_KEY = 'serverPublicKey'.freeze
9
10
  KEY_PRIVATE_KEY = 'privateKey'.freeze
10
11
  KEY_APP_KEY = 'appKey'.freeze
11
12
  KEY_OWNER_PUBLIC_KEY = 'appOwnerPublicKey'.freeze
@@ -18,6 +19,7 @@ module KeeperSecretsManager
18
19
  KEY_CLIENT_KEY,
19
20
  KEY_HOSTNAME,
20
21
  KEY_SERVER_PUBLIC_KEY_ID,
22
+ KEY_SERVER_PUBLIC_KEY,
21
23
  KEY_PRIVATE_KEY,
22
24
  KEY_APP_KEY,
23
25
  KEY_OWNER_PUBLIC_KEY,