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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +83 -10
- data/Gemfile +3 -0
- data/README.md +75 -270
- data/bin/console +47 -0
- data/keeper_secrets_manager.gemspec +36 -0
- data/lib/keeper_secrets_manager/cache.rb +139 -0
- data/lib/keeper_secrets_manager/config_keys.rb +2 -0
- data/lib/keeper_secrets_manager/core.rb +455 -114
- data/lib/keeper_secrets_manager/crypto.rb +3 -11
- data/lib/keeper_secrets_manager/dto/payload.rb +3 -2
- data/lib/keeper_secrets_manager/dto.rb +326 -8
- data/lib/keeper_secrets_manager/keeper_globals.rb +6 -6
- data/lib/keeper_secrets_manager/notation.rb +105 -3
- data/lib/keeper_secrets_manager/notation_enhancements.rb +2 -0
- data/lib/keeper_secrets_manager/storage.rb +3 -2
- data/lib/keeper_secrets_manager/utils.rb +2 -1
- data/lib/keeper_secrets_manager/version.rb +1 -1
- data/lib/keeper_secrets_manager.rb +8 -0
- metadata +34 -3
|
@@ -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,
|