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,84 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # Add the Ruby SDK to the load path
4
+ require_relative '../lib/keeper_secrets_manager'
5
+
6
+ # Load the SDK
7
+ require 'json'
8
+
9
+ # Your token
10
+ TOKEN = ENV['KSM_TOKEN'] || 'US:YOUR_ONE_TIME_TOKEN_HERE'
11
+
12
+ puts "=== Debugging Keeper Secrets Manager ==="
13
+
14
+ begin
15
+ # Connect to Keeper with debug logging
16
+ puts "\n1. Connecting to Keeper..."
17
+ # Option 1: Use base64 config
18
+ config_base64 = ENV['KSM_CONFIG_BASE64'] || 'YOUR_BASE64_CONFIG_HERE'
19
+ storage_from_b64 = KeeperSecretsManager::Storage::InMemoryStorage.new(config_base64)
20
+ sm = KeeperSecretsManager.new(config: storage_from_b64)
21
+ # Alternative: Use in-memory storage
22
+ # storage = KeeperSecretsManager::Storage::InMemoryStorage.new
23
+ # sm = KeeperSecretsManager.new(token: TOKEN, config: storage)
24
+ #
25
+ # The storage now contains the credentials, so you can reuse it:
26
+ # sm2 = KeeperSecretsManager.new(config: storage) # No token needed!
27
+
28
+ # Get all secrets with raw response
29
+ puts "\n2. Fetching secrets (this calls the internal method)..."
30
+
31
+ # Let's look at what the SDK is actually doing internally
32
+ # Get the raw response to see what's coming from the server
33
+ response = sm.instance_eval do
34
+ fetch_and_decrypt_secrets
35
+ end
36
+
37
+ puts "\n3. Response Analysis:"
38
+ puts " - Total records returned: #{response[:records].length}"
39
+ puts " - Total folders returned: #{response[:folders].length}"
40
+ puts " - Warnings: #{response[:warnings]}"
41
+
42
+ # Check if there are any records at all
43
+ if response[:records].empty?
44
+ puts "\n ⚠️ No Secrets Manager-compatible records found!"
45
+ puts " The warning indicates you have 4 'general' type records."
46
+ puts " These are not accessible via the Secrets Manager SDK."
47
+ else
48
+ puts "\n4. Records found:"
49
+ response[:records].each_with_index do |record, i|
50
+ puts " #{i+1}. #{record.title} (Type: #{record.type}, UID: #{record.uid})"
51
+ end
52
+ end
53
+
54
+ # Try to get records with specific UIDs from the warning
55
+ puts "\n5. Attempting to fetch specific UIDs from warning..."
56
+ warning_uids = ['AwOCl5W2RubtkFSuL72jXg', 'bX7uYVNsujLoPjrvEFA3Rg', 'gSnSFvd_UI9CnK_5kSZ85Q', 'wIT61_GnuBIpUOPxOrgPeQ']
57
+
58
+ warning_uids.each do |uid|
59
+ begin
60
+ records = sm.get_secrets([uid])
61
+ if records.any?
62
+ puts " ✓ Found: #{uid}"
63
+ else
64
+ puts " ✗ Not accessible: #{uid} (general type)"
65
+ end
66
+ rescue => e
67
+ puts " ✗ Error fetching #{uid}: #{e.message}"
68
+ end
69
+ end
70
+
71
+ puts "\n6. Summary:"
72
+ puts " - Your vault contains 'general' type records"
73
+ puts " - These are legacy Keeper records not designed for API access"
74
+ puts " - To use Secrets Manager, create records with specific types like:"
75
+ puts " • login"
76
+ puts " • databaseCredentials"
77
+ puts " • sshKeys"
78
+ puts " • serverCredentials"
79
+ puts " • etc."
80
+
81
+ rescue => e
82
+ puts "\nError: #{e.message}"
83
+ puts e.backtrace.first(5).join("\n")
84
+ end
@@ -0,0 +1,182 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # This script demonstrates the Ruby SDK by listing all secrets in your vault
4
+ # It uses the real config.base64 file to connect to Keeper
5
+
6
+ require_relative 'lib/keeper_secrets_manager'
7
+ require 'base64'
8
+ require 'json'
9
+
10
+ puts "=== Keeper Secrets Manager Ruby SDK Demo ==="
11
+ puts "Connecting to Keeper and listing all secrets..."
12
+ puts
13
+
14
+ begin
15
+ # Load the real configuration
16
+ config_file = File.expand_path('config.base64', __dir__)
17
+ unless File.exist?(config_file)
18
+ puts "Error: config.base64 not found at #{config_file}"
19
+ exit 1
20
+ end
21
+
22
+ config_base64 = File.read(config_file).strip
23
+ config_json = Base64.decode64(config_base64)
24
+ config_data = JSON.parse(config_json)
25
+
26
+ # Display connection info (partial)
27
+ puts "Configuration loaded:"
28
+ puts " Hostname: #{config_data['hostname']}"
29
+ puts " Client ID: #{config_data['clientId'][0..30]}..."
30
+ puts " Has appKey: #{config_data.key?('appKey')}"
31
+ puts " Has privateKey: #{config_data.key?('privateKey')}"
32
+ puts " Has serverPublicKeyId: #{config_data['serverPublicKeyId']}"
33
+ puts
34
+
35
+ # Create storage and SDK instance
36
+ storage = KeeperSecretsManager::Storage::InMemoryStorage.new(config_data)
37
+
38
+ # Enable debug logging
39
+ require 'logger'
40
+ logger = Logger.new(STDOUT)
41
+ logger.level = Logger::DEBUG
42
+
43
+ secrets_manager = KeeperSecretsManager.new(config: storage, logger: logger, log_level: Logger::DEBUG)
44
+
45
+ # Get all secrets
46
+ puts "Fetching all secrets..."
47
+ records = secrets_manager.get_secrets
48
+
49
+ if records.empty?
50
+ puts "\nNo secrets found in the vault."
51
+ else
52
+ puts "\nFound #{records.length} secret(s):"
53
+ puts "-" * 80
54
+
55
+ records.each_with_index do |record, index|
56
+ puts "\n#{index + 1}. #{record.title}"
57
+ puts " UID: #{record.uid}"
58
+ puts " Type: #{record.type}"
59
+
60
+ # Show fields (hide sensitive values)
61
+ if record.fields.any?
62
+ puts " Fields:"
63
+ record.fields.each do |field|
64
+ field_type = field['type']
65
+ field_label = field['label'] ? " (#{field['label']})" : ""
66
+
67
+ # Show field info but mask sensitive data
68
+ case field_type
69
+ when 'login', 'email'
70
+ value = record.get_field_value_single(field_type)
71
+ puts " - #{field_type}#{field_label}: #{value}"
72
+ when 'password', 'secret', 'privateKey'
73
+ puts " - #{field_type}#{field_label}: [HIDDEN]"
74
+ when 'url'
75
+ values = record.get_field_value(field_type)
76
+ if values.length == 1
77
+ puts " - #{field_type}#{field_label}: #{values.first}"
78
+ else
79
+ puts " - #{field_type}#{field_label}: #{values.length} URLs"
80
+ end
81
+ when 'host'
82
+ host = record.get_field_value_single(field_type)
83
+ if host.is_a?(Hash)
84
+ puts " - #{field_type}#{field_label}: #{host['hostName']}:#{host['port'] || '22'}"
85
+ end
86
+ when 'phone'
87
+ phone = record.get_field_value_single(field_type)
88
+ if phone.is_a?(Hash)
89
+ puts " - #{field_type}#{field_label}: #{phone['number']} (#{phone['region']})"
90
+ end
91
+ when 'name'
92
+ name = record.get_field_value_single(field_type)
93
+ if name.is_a?(Hash)
94
+ full_name = [name['first'], name['middle'], name['last']].compact.join(' ')
95
+ puts " - #{field_type}#{field_label}: #{full_name}"
96
+ end
97
+ else
98
+ puts " - #{field_type}#{field_label}: [#{field['value']&.length || 0} value(s)]"
99
+ end
100
+ end
101
+ end
102
+
103
+ # Show custom fields
104
+ if record.custom.any?
105
+ puts " Custom Fields:"
106
+ record.custom.each do |field|
107
+ label = field['label'] || field['type']
108
+ value_count = field['value']&.length || 0
109
+ puts " - #{label}: [#{value_count} value(s)]"
110
+ end
111
+ end
112
+
113
+ # Show notes (first 50 chars)
114
+ if record.notes && !record.notes.empty?
115
+ notes_preview = record.notes[0..50]
116
+ notes_preview += "..." if record.notes.length > 50
117
+ puts " Notes: #{notes_preview}"
118
+ end
119
+
120
+ # Show file count
121
+ if record.files && record.files.any?
122
+ puts " Files: #{record.files.length} attachment(s)"
123
+ end
124
+ end
125
+
126
+ puts "\n" + "-" * 80
127
+ end
128
+
129
+ # Get folders
130
+ puts "\nFetching folders..."
131
+ folders = secrets_manager.get_folders
132
+
133
+ if folders.empty?
134
+ puts "No folders found."
135
+ else
136
+ puts "Found #{folders.length} folder(s):"
137
+ folders.each do |folder|
138
+ puts " - #{folder.name} (#{folder.uid})"
139
+ end
140
+ end
141
+
142
+ # Demonstrate notation
143
+ if records.any?
144
+ puts "\n" + "=" * 80
145
+ puts "Notation Examples (using first record):"
146
+ puts "=" * 80
147
+
148
+ first_record = records.first
149
+
150
+ # Try various notations
151
+ notations = [
152
+ "keeper://#{first_record.uid}/type",
153
+ "keeper://#{first_record.uid}/title",
154
+ "keeper://#{first_record.uid}/field/login"
155
+ ]
156
+
157
+ notations.each do |notation|
158
+ begin
159
+ value = secrets_manager.get_notation(notation)
160
+ puts "#{notation}"
161
+ puts " => #{value}"
162
+ rescue => e
163
+ puts "#{notation}"
164
+ puts " => Error: #{e.message}"
165
+ end
166
+ end
167
+ end
168
+
169
+ puts "\n✅ SDK is working correctly!"
170
+ puts "\nCapabilities demonstrated:"
171
+ puts "- ✓ Connected to Keeper"
172
+ puts "- ✓ Retrieved all secrets"
173
+ puts "- ✓ Accessed field values"
174
+ puts "- ✓ Listed folders"
175
+ puts "- ✓ Notation parsing"
176
+
177
+ rescue => e
178
+ puts "\n❌ Error: #{e.class} - #{e.message}"
179
+ puts "\nBacktrace:"
180
+ puts e.backtrace.first(5).join("\n")
181
+ exit 1
182
+ end
@@ -0,0 +1,100 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # Add the Ruby SDK to the load path
4
+ $LOAD_PATH.unshift('/Users/mustinov/Source/secrets-manager/sdk/ruby/lib')
5
+
6
+ # Load the SDK
7
+ require 'keeper_secrets_manager'
8
+ require 'fileutils'
9
+
10
+ # Helper method to sanitize filename
11
+ def sanitize_filename(filename)
12
+ # Remove or replace invalid characters
13
+ filename.gsub(/[<>:"|?*\/\\]/, '_').strip
14
+ end
15
+
16
+ begin
17
+ # Connect to Keeper
18
+ puts "Connecting to Keeper..."
19
+ # IMPORTANT: Replace with your own configuration
20
+ # Option 1: Use one-time token
21
+ # token = 'US:YOUR_ONE_TIME_TOKEN_HERE'
22
+ # sm = KeeperSecretsManager.from_token(token)
23
+ #
24
+ # Option 2: Use base64 config
25
+ config_base64 = ENV['KSM_CONFIG_BASE64'] || 'YOUR_BASE64_CONFIG_HERE'
26
+ storage_from_b64 = KeeperSecretsManager::Storage::InMemoryStorage.new(config_base64)
27
+ sm = KeeperSecretsManager.new(config: storage_from_b64)
28
+
29
+ # Get all secrets
30
+ puts "\nFetching secrets..."
31
+ response = sm.get_secrets(full_response: true)
32
+ secrets = response.records
33
+
34
+ # Find secrets with files
35
+ secrets_with_files = secrets.select { |s| s.files && s.files.any? }
36
+
37
+ if secrets_with_files.empty?
38
+ puts "\nNo secrets with files found."
39
+ exit
40
+ end
41
+
42
+ puts "\nFound #{secrets_with_files.length} secrets with files:"
43
+
44
+ # Create downloads directory
45
+ download_dir = "downloads"
46
+ FileUtils.mkdir_p(download_dir)
47
+
48
+ total_files = 0
49
+ downloaded_files = 0
50
+ failed_files = 0
51
+
52
+ secrets_with_files.each do |secret|
53
+ puts "\n📁 #{secret.title} (#{secret.files.length} files)"
54
+
55
+ secret.files.each do |file|
56
+ total_files += 1
57
+ file_name = file['title'] || file['name'] || 'unnamed'
58
+ file_size = file['size'] || 0
59
+ file_uid = file['fileUid'] || file['uid']
60
+
61
+ print " - #{file_name} (#{file_size} bytes) ... "
62
+
63
+ if file_uid
64
+ begin
65
+ # Download the file (pass the whole file object)
66
+ downloaded_file = sm.download_file(file)
67
+
68
+ # Create subdirectory for the secret
69
+ secret_dir = File.join(download_dir, sanitize_filename(secret.title))
70
+ FileUtils.mkdir_p(secret_dir)
71
+
72
+ # Save the file (sanitize the filename to remove path components)
73
+ safe_filename = File.basename(downloaded_file['name'] || file_name)
74
+ file_path = File.join(secret_dir, safe_filename)
75
+ File.binwrite(file_path, downloaded_file['data'])
76
+
77
+ downloaded_files += 1
78
+ puts "✅ Saved"
79
+ rescue => e
80
+ failed_files += 1
81
+ puts "❌ Failed: #{e.message}"
82
+ end
83
+ else
84
+ failed_files += 1
85
+ puts "❌ No file UID"
86
+ end
87
+ end
88
+ end
89
+
90
+ puts "\n" + "=" * 60
91
+ puts "Download Summary:"
92
+ puts " Total files: #{total_files}"
93
+ puts " ✅ Downloaded: #{downloaded_files}"
94
+ puts " ❌ Failed: #{failed_files}"
95
+ puts "\nFiles saved to: #{File.expand_path(download_dir)}/"
96
+
97
+ rescue => e
98
+ puts "\nError: #{e.message}"
99
+ puts e.backtrace
100
+ end
@@ -0,0 +1,94 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require_relative '../lib/keeper_secrets_manager'
4
+
5
+ # Example showing flexible record creation in multiple ways
6
+
7
+ # Method 1: Direct hash-based creation (JavaScript-style)
8
+ record1 = KeeperSecretsManager::Dto::KeeperRecord.new(
9
+ title: 'My Login',
10
+ type: 'login',
11
+ fields: [
12
+ { 'type' => 'login', 'value' => ['my_username'] },
13
+ { 'type' => 'password', 'value' => ['my_password'] },
14
+ { 'type' => 'url', 'value' => ['https://example.com'] },
15
+ {
16
+ 'type' => 'host',
17
+ 'value' => [{ 'hostName' => '192.168.1.1', 'port' => '8080' }],
18
+ 'label' => 'Custom Host'
19
+ }
20
+ ],
21
+ custom: [
22
+ {
23
+ 'type' => 'phone',
24
+ 'value' => [{ 'region' => 'US', 'number' => '555-1234' }],
25
+ 'label' => 'Work Phone'
26
+ }
27
+ ],
28
+ notes: 'Example login record'
29
+ )
30
+
31
+ # Method 2: Using field helpers (optional convenience)
32
+ record2 = KeeperSecretsManager::Dto::KeeperRecord.new(
33
+ title: 'My Server',
34
+ type: 'sshKeys'
35
+ )
36
+
37
+ # Add fields dynamically
38
+ record2.set_field('login', 'root')
39
+ record2.set_field('sshKey', {
40
+ 'privateKey' => '-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----',
41
+ 'publicKey' => 'ssh-rsa AAAAB3...'
42
+ })
43
+
44
+ # Method 3: Using dynamic accessors (Ruby magic)
45
+ record3 = KeeperSecretsManager::Dto::KeeperRecord.new(
46
+ title: 'Dynamic Record',
47
+ type: 'login'
48
+ )
49
+
50
+ # These create fields dynamically!
51
+ record3.login = 'john_doe'
52
+ record3.password = 'secure_pass123'
53
+ record3.url = 'https://myapp.com'
54
+
55
+ # Method 4: Mixing approaches
56
+ record4 = KeeperSecretsManager::Dto::KeeperRecord.new(
57
+ title: 'Payment Info',
58
+ type: 'bankCard'
59
+ )
60
+
61
+ # Use helper for complex field
62
+ card_field = KeeperSecretsManager::FieldTypes::Helpers.payment_card(
63
+ number: '4111111111111111',
64
+ expiration_date: '12/25',
65
+ security_code: '123',
66
+ cardholder_name: 'John Doe'
67
+ )
68
+
69
+ # Add to record
70
+ record4.fields << card_field.to_h
71
+
72
+ # Add custom field with any structure
73
+ record4.set_field('customData', {
74
+ 'someKey' => 'someValue',
75
+ 'nested' => {
76
+ 'data' => 'structure'
77
+ }
78
+ }, 'My Custom Data', true) # true = custom field
79
+
80
+ # Access field values
81
+ puts "Login: #{record1.get_field_value_single('login')}"
82
+ puts "Password field: #{record1.get_field('password')}"
83
+ puts "All custom fields: #{record1.custom}"
84
+
85
+ # Dynamic accessor
86
+ puts "URL from record3: #{record3.url}"
87
+
88
+ # Access complex field values
89
+ host_field = record1.get_field_value_single('host')
90
+ puts "Host: #{host_field['hostName']}:#{host_field['port']}" if host_field
91
+
92
+ # Convert to hash for API
93
+ puts "\nRecord as hash for API:"
94
+ puts record1.to_h.to_json
@@ -0,0 +1,109 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require_relative '../lib/keeper_secrets_manager'
4
+
5
+ # This example demonstrates folder hierarchy functionality in the Ruby SDK
6
+
7
+ begin
8
+ # Initialize client (assumes KSM_CONFIG environment variable is set)
9
+ client = KeeperSecretsManager.new
10
+
11
+ puts "=== Folder Hierarchy Demo ==="
12
+ puts ""
13
+
14
+ # Get all folders
15
+ folders = client.get_folders
16
+ puts "Found #{folders.length} folders"
17
+ puts ""
18
+
19
+ # Get folder manager for advanced operations
20
+ fm = client.folder_manager
21
+
22
+ # Build and display folder tree
23
+ puts "Folder Tree Structure:"
24
+ puts "-" * 40
25
+ fm.print_tree
26
+ puts ""
27
+
28
+ # Demonstrate path retrieval
29
+ if folders.any?
30
+ first_folder = folders.first
31
+ path = client.get_folder_path(first_folder.uid)
32
+ puts "Path to '#{first_folder.name}': #{path}"
33
+
34
+ # Show ancestors
35
+ ancestors = fm.get_ancestors(first_folder.uid)
36
+ if ancestors.any?
37
+ puts "Ancestors of '#{first_folder.name}':"
38
+ ancestors.each { |a| puts " - #{a.name}" }
39
+ else
40
+ puts "No ancestors (root folder or orphan)"
41
+ end
42
+
43
+ # Show descendants
44
+ descendants = fm.get_descendants(first_folder.uid)
45
+ if descendants.any?
46
+ puts "Descendants of '#{first_folder.name}':"
47
+ descendants.each { |d| puts " - #{d.name}" }
48
+ else
49
+ puts "No descendants (leaf folder)"
50
+ end
51
+ end
52
+
53
+ puts ""
54
+
55
+ # Find folder by name
56
+ puts "Searching for folders:"
57
+ puts "-" * 40
58
+
59
+ # Example: Find a folder named "Personal"
60
+ personal = client.find_folder_by_name("Personal")
61
+ if personal
62
+ puts "Found folder 'Personal' with UID: #{personal.uid}"
63
+ puts "Path: #{client.get_folder_path(personal.uid)}"
64
+ else
65
+ puts "No folder named 'Personal' found"
66
+ end
67
+
68
+ # Get all records organized by folder
69
+ puts ""
70
+ puts "Records by Folder:"
71
+ puts "-" * 40
72
+
73
+ response = client.get_secrets
74
+
75
+ # Group records by folder
76
+ records_by_folder = {}
77
+ root_records = []
78
+
79
+ response.records.each do |record|
80
+ if record.folder_uid && !record.folder_uid.empty?
81
+ records_by_folder[record.folder_uid] ||= []
82
+ records_by_folder[record.folder_uid] << record
83
+ else
84
+ root_records << record
85
+ end
86
+ end
87
+
88
+ # Display root records
89
+ if root_records.any?
90
+ puts "Root Records (no folder):"
91
+ root_records.each { |r| puts " - #{r.title}" }
92
+ puts ""
93
+ end
94
+
95
+ # Display records in each folder
96
+ folders.each do |folder|
97
+ folder_records = records_by_folder[folder.uid]
98
+ if folder_records && folder_records.any?
99
+ path = client.get_folder_path(folder.uid)
100
+ puts "Records in '#{path}':"
101
+ folder_records.each { |r| puts " - #{r.title}" }
102
+ puts ""
103
+ end
104
+ end
105
+
106
+ rescue => e
107
+ puts "Error: #{e.message}"
108
+ puts e.backtrace.first(5).join("\n")
109
+ end