Class: KeeperSecretsManager::Core::SecretsManager
- Inherits:
-
Object
- Object
- KeeperSecretsManager::Core::SecretsManager
- Defined in:
- lib/keeper_secrets_manager/core.rb
Constant Summary collapse
- NOTATION_PREFIX =
'keeper'.freeze
- DEFAULT_KEY_ID =
'7'.freeze
- MAX_THROTTLE_RETRIES =
Throttle retry (KSM-876 / KSM-883). The backend throttles HTTP 403 "error":"throttled" per clientId+endpoint (100 requests / 10s window; memcached TTL 10s that resets on every request, so the counter only clears after 10s of silence).
5- BASE_THROTTLE_DELAY_SEC =
1s safety margin over the backend's 10s memcached TTL
11- MAX_THROTTLE_DELAY_SEC =
cap on server-supplied retry_after (BASE * 2**4)
176- INFLATE_REF_TYPES =
Field types that can be inflated
{ 'addressRef' => ['address'], 'cardRef' => %w[paymentCard text pinCode addressRef] }.freeze
Instance Attribute Summary collapse
-
#config ⇒ Object
readonly
Returns the value of attribute config.
-
#hostname ⇒ Object
readonly
Returns the value of attribute hostname.
-
#verify_ssl_certs ⇒ Object
readonly
Returns the value of attribute verify_ssl_certs.
Instance Method Summary collapse
-
#complete_transaction(record_uid, rollback: false) ⇒ Object
Complete transaction - commit or rollback Used after update_secret with transaction_type to finalize PAM rotation.
-
#create_folder(folder_name, parent_uid: nil) ⇒ Object
Create folder.
- #create_secret(record_data, options = nil) ⇒ Object
- #create_secret_with_options(create_options, record_data, folders: nil) ⇒ Object
-
#delete_folder(folder_uids, force: false) ⇒ Object
Delete folders.
-
#delete_secret(record_uids) ⇒ Object
Delete secrets.
-
#download_encrypted_file(url) ⇒ Object
Download encrypted file from URL.
-
#download_file(file_data) ⇒ Object
Download file from record's file data.
-
#download_thumbnail(file_data) ⇒ Object
Download file thumbnail.
-
#fetch_and_decrypt_folders ⇒ Object
Fetch and decrypt folders from dedicated endpoint.
-
#find_folder_by_name(name, parent_uid: nil) ⇒ Object
Find folder by name (convenience method).
-
#folder_manager ⇒ Object
Get folder hierarchy manager.
-
#get_file_data(file_uid) ⇒ Object
Get file metadata from server.
-
#get_folder_path(folder_uid) ⇒ Object
Get folder path (convenience method).
-
#get_folders ⇒ Object
Get all folders.
- #get_inflate_ref_types(field_type) ⇒ Object
-
#get_notation(notation_uri) ⇒ Object
Get notation value.
- #get_notation_results(notation_uri) ⇒ Object
-
#get_secret_by_title(title) ⇒ Object
Get first secret by title.
-
#get_secrets(uids = nil, full_response: false, request_links: false) ⇒ Object
Get secrets with optional filtering.
-
#get_secrets_by_title(title) ⇒ Object
Get secrets by title.
-
#get_secrets_with_options(query_options = nil, full_response: false) ⇒ Object
Get secrets with query options.
- #inflate_field_value(uids, replace_fields) ⇒ Object
-
#initialize(options = {}) ⇒ SecretsManager
constructor
A new instance of SecretsManager.
- #save(record, transaction_type: nil, links_to_remove: nil) ⇒ Object
- #save_with_options(record, update_options = nil) ⇒ Object
-
#try_get_notation(notation_uri) ⇒ Object
Get notation value without raising exceptions (convenience method) Returns empty array if notation is invalid or record not found.
-
#try_get_notation_results(notation_uri) ⇒ Object
Like get_notation_results but never raises; logs errors and returns [].
-
#update_folder(folder_uid, folder_name) ⇒ Object
Update folder.
-
#update_secret(record, transaction_type: 'general') ⇒ Object
Update existing secret (convenience wrapper).
-
#update_secret_with_options(record, update_options = nil) ⇒ Object
Update existing secret with UpdateOptions.
-
#upload_file(owner_record_uid, file_data, file_name, file_title = nil) ⇒ Object
Upload file.
-
#upload_file_from_path(owner_record_uid, file_path, file_title: nil) ⇒ Object
Upload file from disk path (convenience method) Reads file from disk and uploads to specified record.
Constructor Details
#initialize(options = {}) ⇒ SecretsManager
Returns a new instance of SecretsManager.
28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 |
# File 'lib/keeper_secrets_manager/core.rb', line 28 def initialize( = {}) # Check Ruby version raise Error, 'KSM SDK requires Ruby 3.1 or greater' if RUBY_VERSION < '3.1' # Check AES-GCM support begin OpenSSL::Cipher.new('AES-256-GCM') rescue RuntimeError => e if e..include?('unsupported cipher') raise Error, "KSM SDK requires AES-GCM support. Your Ruby/OpenSSL version (#{OpenSSL::OPENSSL_LIBRARY_VERSION}) does not support AES-256-GCM. Please upgrade to Ruby 2.7+ or use a Ruby compiled with OpenSSL 1.1.0+" end raise e end @token = nil @hostname = nil @verify_ssl_certs = .fetch(:verify_ssl_certs, true) @custom_post_function = [:custom_post_function] # optional custom server public key overrides (isolated deployments). # Precedence, highest first: these programmatic params > OTT segments > pre-existing config. @server_public_key_override = [:server_public_key] @server_public_key_id_override = [:server_public_key_id] # Set up proxy configuration # Priority: explicit proxy_url parameter > HTTPS_PROXY env var > no proxy @proxy_url = [:proxy_url] || ENV['HTTPS_PROXY'] || ENV['https_proxy'] if @proxy_url begin proxy_uri = URI.parse(@proxy_url) unless proxy_uri.is_a?(URI::HTTP) && !proxy_uri.host.to_s.empty? raise ArgumentError, "Invalid proxy_url '#{@proxy_url}': must be a valid http or https URL with a host (e.g., http://proxy.example.com:8080)" end rescue URI::InvalidURIError => e raise ArgumentError, "Invalid proxy_url '#{@proxy_url}': #{e.}" end end # Set up logging @logger = [:logger] || Logger.new(STDOUT) @logger.level = [:log_level] || Logger::WARN @logger.debug("Proxy configuration: #{@proxy_url ? @proxy_url : 'none'}") if @proxy_url # Handle configuration config = [:config] token = [:token] # Check environment variable if no config provided config = Storage::InMemoryStorage.new(ENV['KSM_CONFIG']) if config.nil? && ENV['KSM_CONFIG'] # If we have config, check if it's already initialized if config @config = config # Check if already bound (has client ID and app key) if @config.get_string(ConfigKeys::KEY_CLIENT_ID) && @config.get_bytes(ConfigKeys::KEY_APP_KEY) @logger.debug('Using existing credentials from config') elsif token # Config exists but not bound, use token to bind @logger.debug('Config provided but not bound, using token to initialize') process_token_binding(token, [:hostname]) else @logger.warn('Config provided but no credentials found and no token provided') end elsif token # No config provided, create new one with token @logger.debug('No config provided, creating new one with token') process_token_binding(token, [:hostname]) @config ||= Storage::InMemoryStorage.new else # No config and no token raise Error, 'Either token or initialized config must be provided' end # programmatic custom key wins over token/config (applied once @config is set). @config.save_string(ConfigKeys::KEY_SERVER_PUBLIC_KEY, @server_public_key_override) if @server_public_key_override && !@server_public_key_override.empty? @config.save_string(ConfigKeys::KEY_SERVER_PUBLIC_KEY_ID, @server_public_key_id_override.to_s) if @server_public_key_id_override # if the configured key id is not in the built-in table AND no custom key backs it, # fall back to the default (mirrors Python). A persisted custom serverPublicKey is preserved. current_key_id = @config.get_string(ConfigKeys::KEY_SERVER_PUBLIC_KEY_ID) custom_server_key = @config.get_string(ConfigKeys::KEY_SERVER_PUBLIC_KEY) if current_key_id && !KeeperGlobals::KEEPER_PUBLIC_KEYS.key?(current_key_id) && (custom_server_key.nil? || custom_server_key.empty?) @logger.debug("Public key id #{current_key_id} unknown and no custom key present; using default #{DEFAULT_KEY_ID}") @config.save_string(ConfigKeys::KEY_SERVER_PUBLIC_KEY_ID, DEFAULT_KEY_ID) end # Override hostname if provided if [:hostname] @hostname = [:hostname] @config.save_string(ConfigKeys::KEY_HOSTNAME, @hostname) else @hostname = @config.get_string(ConfigKeys::KEY_HOSTNAME) || KeeperGlobals::DEFAULT_SERVER end end |
Instance Attribute Details
#config ⇒ Object (readonly)
Returns the value of attribute config.
10 11 12 |
# File 'lib/keeper_secrets_manager/core.rb', line 10 def config @config end |
#hostname ⇒ Object (readonly)
Returns the value of attribute hostname.
10 11 12 |
# File 'lib/keeper_secrets_manager/core.rb', line 10 def hostname @hostname end |
#verify_ssl_certs ⇒ Object (readonly)
Returns the value of attribute verify_ssl_certs.
10 11 12 |
# File 'lib/keeper_secrets_manager/core.rb', line 10 def verify_ssl_certs @verify_ssl_certs end |
Instance Method Details
#complete_transaction(record_uid, rollback: false) ⇒ Object
Complete transaction - commit or rollback Used after update_secret with transaction_type to finalize PAM rotation
375 376 377 378 379 380 381 382 383 384 385 386 387 388 |
# File 'lib/keeper_secrets_manager/core.rb', line 375 def complete_transaction(record_uid, rollback: false) @logger.debug("Completing transaction for record #{record_uid}, rollback: #{rollback}") # Prepare payload payload = prepare_complete_transaction_payload(record_uid) # Route to different endpoints based on rollback parameter endpoint = rollback ? 'rollback_secret_update' : 'finalize_secret_update' # Send request post_query(endpoint, payload) true end |
#create_folder(folder_name, parent_uid: nil) ⇒ Object
Create folder
476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 |
# File 'lib/keeper_secrets_manager/core.rb', line 476 def create_folder(folder_name, parent_uid: nil) raise ArgumentError, 'parent_uid is required to create a folder' unless parent_uid # Get folders to find parent's shared folder key folders = get_folders # Find parent folder parent_folder = folders.find { |f| f.uid == parent_uid } raise Error, "Parent folder #{parent_uid} not found" unless parent_folder # Determine if parent is a shared folder root (no parent_uid) is_shared_root = parent_folder.parent_uid.nil? || parent_folder.parent_uid.empty? # Find the shared folder root by traversing up the hierarchy if is_shared_root # Parent is the shared root, so new folder is at root level shared_folder_uid = parent_uid actual_parent_uid = nil # nil for root-level folders else # Parent is a subfolder, traverse up to find shared root shared_folder_uid = parent_uid current_folder = parent_folder while current_folder.parent_uid && !current_folder.parent_uid.empty? parent = folders.find { |f| f.uid == current_folder.parent_uid } break unless parent shared_folder_uid = current_folder.parent_uid current_folder = parent end actual_parent_uid = parent_uid # Subfolder creation end # Get shared folder's key (the root folder's key) shared_folder = folders.find { |f| f.uid == shared_folder_uid } raise Error, "Shared folder #{shared_folder_uid} not found" unless shared_folder shared_folder_key = shared_folder.folder_key raise Error, "Shared folder key missing for #{shared_folder_uid}" unless shared_folder_key # Generate new folder UID and key folder_uid = Utils.generate_uid folder_key = Crypto.generate_encryption_key_bytes # Prepare folder data folder_data = { 'name' => folder_name } # Encrypt folder data with NEW folder's key using AES-CBC encrypted_data = Crypto.encrypt_aes_cbc( Utils.dict_to_json(folder_data), folder_key ) # Encrypt folder key with SHARED folder's key using AES-CBC encrypted_folder_key = Crypto.encrypt_aes_cbc(folder_key, shared_folder_key) # Prepare payload payload = prepare_create_folder_payload( folder_uid: folder_uid, shared_folder_uid: shared_folder_uid, encrypted_folder_key: encrypted_folder_key, data: encrypted_data, parent_uid: actual_parent_uid # nil for root, subfolder UID for nested ) post_query('create_folder', payload) folder_uid end |
#create_secret(record_data, options = nil) ⇒ Object
276 277 278 279 280 281 282 283 284 285 |
# File 'lib/keeper_secrets_manager/core.rb', line 276 def create_secret(record_data, = nil) = if .is_a?(Dto::CreateOptions) elsif .is_a?(String) Dto::CreateOptions.new(folder_uid: ) else Dto::CreateOptions.new end (, record_data) end |
#create_secret_with_options(create_options, record_data, folders: nil) ⇒ Object
245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 |
# File 'lib/keeper_secrets_manager/core.rb', line 245 def (, record_data, folders: nil) raise ArgumentError, 'folder_uid is required to create a record' unless &.folder_uid folders ||= get_folders folder = folders.find { |f| f.uid == .folder_uid } raise Error, "Folder #{.folder_uid} not found or not accessible" unless folder folder_key = folder.folder_key raise Error, "Unable to create record - folder key for #{.folder_uid} is missing" unless folder_key record_uid = Utils.generate_uid record_key = Crypto.generate_encryption_key_bytes record = record_data.is_a?(Dto::KeeperRecord) ? record_data.to_h : record_data encrypted_data = Crypto.encrypt_aes_gcm(Utils.dict_to_json(record), record_key) payload = prepare_create_payload( record_uid: record_uid, record_key: record_key, folder_uid: .folder_uid, folder_key: folder_key, data: encrypted_data, subfolder_uid: .subfolder_uid ) post_query('create_secret', payload) record_uid end |
#delete_folder(folder_uids, force: false) ⇒ Object
Delete folders
579 580 581 582 583 584 585 586 587 588 589 590 591 592 |
# File 'lib/keeper_secrets_manager/core.rb', line 579 def delete_folder(folder_uids, force: false) folder_uids = [folder_uids] if folder_uids.is_a?(String) payload = prepare_delete_folder_payload(folder_uids, force) response = post_query('delete_folder', payload) result = JSON.parse(response) folders = result['folders'] || [] folders.each do |f| next if f['responseCode'] == 'ok' @logger.error("Failed to delete folder #{f['folderUid']}: #{f['responseCode']} #{f['errorMessage']}") end folders end |
#delete_secret(record_uids) ⇒ Object
Delete secrets
391 392 393 394 395 396 397 398 399 400 401 402 403 404 |
# File 'lib/keeper_secrets_manager/core.rb', line 391 def delete_secret(record_uids) record_uids = [record_uids] if record_uids.is_a?(String) payload = prepare_delete_payload(record_uids) response = post_query('delete_secret', payload) result = JSON.parse(response) records = result['records'] || [] records.each do |r| next if r['responseCode'] == 'ok' @logger.error("Failed to delete record #{r['recordUid']}: #{r['responseCode']} #{r['errorMessage']}") end records end |
#download_encrypted_file(url) ⇒ Object
Download encrypted file from URL
815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 |
# File 'lib/keeper_secrets_manager/core.rb', line 815 def download_encrypted_file(url) uri = URI(url) @logger.debug("Downloading file from URL: #{url}") request = Net::HTTP::Get.new(uri) http = create_http_client(uri) configure_http_ssl(http) response = http.request(request) @logger.debug("Download response status: #{response.code}") if response.code == '200' response.body else raise Error, "Failed to download file: #{response.code} #{response.}" end end |
#download_file(file_data) ⇒ Object
Download file from record's file data
728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 |
# File 'lib/keeper_secrets_manager/core.rb', line 728 def download_file(file_data) # Extract file metadata (already decrypted) file_uid = file_data['fileUid'] file_url = file_data['url'] file_name = file_data['name'] || file_data['title'] || 'unnamed' raise Error, "No download URL available for file #{file_uid}" unless file_url # The file key should already be decrypted (base64 encoded) file_key = Utils.base64_to_bytes(file_data['fileKey']) # Download the encrypted file content encrypted_content = download_encrypted_file(file_url) # Decrypt the file content with the file key decrypted_content = Crypto.decrypt_aes_gcm(encrypted_content, file_key) # Return file info and data { 'name' => file_name, 'title' => file_data['title'] || file_name, 'type' => file_data['type'], 'size' => file_data['size'] || decrypted_content.bytesize, 'data' => decrypted_content } end |
#download_thumbnail(file_data) ⇒ Object
Download file thumbnail
756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 |
# File 'lib/keeper_secrets_manager/core.rb', line 756 def download_thumbnail(file_data) if file_data.is_a?(Dto::KeeperFile) file_uid = file_data.uid thumbnail_url = file_data.thumbnail_url file_key_str = file_data.file_key else file_uid = file_data['fileUid'] || file_data['uid'] thumbnail_url = file_data['thumbnailUrl'] || file_data['thumbnail_url'] file_key_str = file_data['fileKey'] || file_data['file_key'] end raise ArgumentError, 'File UID is required' unless file_uid raise Error, "No thumbnail URL available for file #{file_uid}" unless thumbnail_url raise Error, "File key not available for #{file_uid}" unless file_key_str file_key = Utils.base64_to_bytes(file_key_str) encrypted_content = download_encrypted_file(thumbnail_url) decrypted_content = Crypto.decrypt_aes_gcm(encrypted_content, file_key) { 'file_uid' => file_uid, 'data' => decrypted_content, 'size' => decrypted_content.bytesize } end |
#fetch_and_decrypt_folders ⇒ Object
Fetch and decrypt folders from dedicated endpoint
163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 |
# File 'lib/keeper_secrets_manager/core.rb', line 163 def fetch_and_decrypt_folders # Prepare payload for get_folders endpoint (no filters) payload = prepare_get_payload(nil) # Make request to get_folders endpoint response_json = post_query('get_folders', payload) response_dict = JSON.parse(response_json) # Get app key for decryption app_key_str = @config.get_string(ConfigKeys::KEY_APP_KEY) # If we have app key directly (one-time token binding), use it if app_key_str && !app_key_str.empty? app_key = Utils.base64_to_bytes(app_key_str) else # Otherwise decrypt it using client key app_key_encrypted = Utils.base64_to_bytes(@config.get_string(ConfigKeys::KEY_ENCRYPTED_APP_KEY)) client_key = Utils.base64_to_bytes(@config.get_string(ConfigKeys::KEY_CLIENT_KEY)) app_key = Crypto.decrypt_aes_gcm(app_key_encrypted, client_key) end # Decrypt folders - need to handle them in order for shared folder keys folders = [] response_folders = response_dict['folders'] || [] response_folders.each do |encrypted_folder| folder_uid = encrypted_folder['folderUid'] folder_parent = encrypted_folder['parent'] # Decrypt folder key based on whether it has a parent if !folder_parent || folder_parent.empty? # Root folder - decrypt with app key folder_key_encrypted = Utils.base64_to_bytes(encrypted_folder['folderKey']) folder_key = Crypto.decrypt_aes_gcm(folder_key_encrypted, app_key) else # Child folder - decrypt with parent's shared folder key shared_folder_key = get_shared_folder_key(folders, response_folders, folder_parent) unless shared_folder_key @logger.error("Cannot find shared folder key for parent #{folder_parent}") next end folder_key_encrypted = Utils.base64_to_bytes(encrypted_folder['folderKey']) folder_key = Crypto.decrypt_aes_cbc(folder_key_encrypted, shared_folder_key) end # Decrypt folder data if present folder_name = '' if encrypted_folder['data'] && !encrypted_folder['data'].empty? data_encrypted = Utils.base64_to_bytes(encrypted_folder['data']) data_json = Crypto.decrypt_aes_cbc(data_encrypted, folder_key) data = JSON.parse(data_json) folder_name = data['name'] || '' end # Create folder object folder = Dto::KeeperFolder.new( 'folderUid' => folder_uid, 'name' => folder_name, 'folderKey' => folder_key, 'parent' => folder_parent, 'records' => [] ) folders << folder rescue StandardError => e @logger.error("Failed to decrypt folder #{encrypted_folder['folderUid']}: #{e.}") end folders end |
#find_folder_by_name(name, parent_uid: nil) ⇒ Object
Find folder by name (convenience method)
606 607 608 |
# File 'lib/keeper_secrets_manager/core.rb', line 606 def find_folder_by_name(name, parent_uid: nil) folder_manager.find_folder_by_name(name, parent_uid: parent_uid) end |
#folder_manager ⇒ Object
Get folder hierarchy manager
595 596 597 598 |
# File 'lib/keeper_secrets_manager/core.rb', line 595 def folder_manager folders = get_folders FolderManager.new(folders) end |
#get_file_data(file_uid) ⇒ Object
Get file metadata from server
783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 |
# File 'lib/keeper_secrets_manager/core.rb', line 783 def get_file_data(file_uid) payload = prepare_get_payload(nil) payload.file_uids = [file_uid] response = post_query('get_files', payload) response_dict = JSON.parse(response) if response_dict['files'] && !response_dict['files'].empty? file_data = response_dict['files'].first # Decrypt file metadata # Get app key for decryption app_key_str = @config.get_string(ConfigKeys::KEY_APP_KEY) if app_key_str && !app_key_str.empty? app_key = Utils.base64_to_bytes(app_key_str) else # Decrypt app key with client key app_key_encrypted = Utils.base64_to_bytes(@config.get_string(ConfigKeys::KEY_ENCRYPTED_APP_KEY)) client_key = get_client_key app_key = Crypto.decrypt_aes_gcm(app_key_encrypted, client_key) end encrypted_data = Utils.base64_to_bytes(file_data['data']) decrypted_json = Crypto.decrypt_aes_gcm(encrypted_data, app_key) JSON.parse(decrypted_json).merge('fileKey' => file_data['fileKey']) else raise Error, "File not found: #{file_uid}" end end |
#get_folder_path(folder_uid) ⇒ Object
Get folder path (convenience method)
601 602 603 |
# File 'lib/keeper_secrets_manager/core.rb', line 601 def get_folder_path(folder_uid) folder_manager.get_folder_path(folder_uid) end |
#get_folders ⇒ Object
Get all folders
158 159 160 |
# File 'lib/keeper_secrets_manager/core.rb', line 158 def get_folders fetch_and_decrypt_folders end |
#get_inflate_ref_types(field_type) ⇒ Object
406 407 408 |
# File 'lib/keeper_secrets_manager/core.rb', line 406 def get_inflate_ref_types(field_type) INFLATE_REF_TYPES.fetch(field_type, []) end |
#get_notation(notation_uri) ⇒ Object
Get notation value
448 449 450 451 |
# File 'lib/keeper_secrets_manager/core.rb', line 448 def get_notation(notation_uri) parser = Notation::Parser.new(self) parser.parse(notation_uri) end |
#get_notation_results(notation_uri) ⇒ Object
453 454 455 |
# File 'lib/keeper_secrets_manager/core.rb', line 453 def get_notation_results(notation_uri) Notation::Parser.new(self).get_notation_results(notation_uri) end |
#get_secret_by_title(title) ⇒ Object
Get first secret by title
241 242 243 |
# File 'lib/keeper_secrets_manager/core.rb', line 241 def get_secret_by_title(title) get_secrets_by_title(title).first end |
#get_secrets(uids = nil, full_response: false, request_links: false) ⇒ Object
Get secrets with optional filtering
130 131 132 133 134 135 |
# File 'lib/keeper_secrets_manager/core.rb', line 130 def get_secrets(uids = nil, full_response: false, request_links: false) uids = [uids] if uids.is_a?(String) = Dto::QueryOptions.new(records: uids, folders: nil, request_links: request_links) (, full_response: full_response) end |
#get_secrets_by_title(title) ⇒ Object
Get secrets by title
235 236 237 238 |
# File 'lib/keeper_secrets_manager/core.rb', line 235 def get_secrets_by_title(title) records = get_secrets records.select { |r| r.title == title } end |
#get_secrets_with_options(query_options = nil, full_response: false) ⇒ Object
Get secrets with query options
138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 |
# File 'lib/keeper_secrets_manager/core.rb', line 138 def ( = nil, full_response: false) records_resp = fetch_and_decrypt_secrets() # If just bound, fetch again records_resp = fetch_and_decrypt_secrets() if records_resp.just_bound # Log warnings records_resp.warnings&.each { |warning| @logger.warn(warning) } # Log bad records/folders if records_resp.errors&.any? records_resp.errors.each do |error| @logger.error("Error: #{error}") end end full_response ? records_resp : (records_resp.records || []) end |
#inflate_field_value(uids, replace_fields) ⇒ Object
410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 |
# File 'lib/keeper_secrets_manager/core.rb', line 410 def inflate_field_value(uids, replace_fields) records = get_secrets(uids) lookup = records.each_with_object({}) { |r, h| h[r.uid] = r } uids.filter_map do |uid| record = lookup[uid] next nil unless record new_value = nil replace_fields.each do |field_type| field = record.get_field(field_type) next unless field raw_values = field['value'] || [] next if raw_values.empty? real_value = raw_values.first if INFLATE_REF_TYPES.key?(field_type) inflated = inflate_field_value([real_value], INFLATE_REF_TYPES[field_type]) real_value = inflated.first unless inflated.empty? end label = field['label'] || field_type if new_value.nil? new_value = real_value elsif new_value.is_a?(Hash) && real_value.is_a?(Hash) new_value[label] = real_value else new_value = { label => new_value, field_type => real_value } end end new_value end end |
#save(record, transaction_type: nil, links_to_remove: nil) ⇒ Object
368 369 370 371 |
# File 'lib/keeper_secrets_manager/core.rb', line 368 def save(record, transaction_type: nil, links_to_remove: nil) = Dto::UpdateOptions.new(transaction_type: transaction_type, links_to_remove: links_to_remove) (record, ) end |
#save_with_options(record, update_options = nil) ⇒ Object
340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 |
# File 'lib/keeper_secrets_manager/core.rb', line 340 def (record, = nil) if record.is_a?(Dto::KeeperRecord) record_uid = record.uid record_data = record.to_h record_key = record.record_key revision = record.revision else record_uid = record['uid'] || record[:uid] record_data = record record_key = record['record_key'] || record[:record_key] revision = record['revision'] || record[:revision] || 0 end raise ArgumentError, 'Record UID is required' unless record_uid raise Error, "Record key not available for #{record_uid} - record must be obtained via get_secrets" unless record_key payload = prepare_update_payload( record_uid: record_uid, record_data: record_data, record_key: record_key, revision: revision, update_options: ) post_query('update_secret', payload) true end |
#try_get_notation(notation_uri) ⇒ Object
Get notation value without raising exceptions (convenience method) Returns empty array if notation is invalid or record not found
467 468 469 470 471 472 473 |
# File 'lib/keeper_secrets_manager/core.rb', line 467 def try_get_notation(notation_uri) parser = Notation::Parser.new(self) Array(parser.parse(notation_uri)) rescue NotationError, RecordNotFoundError, StandardError => e @logger.debug("try_get_notation failed for '#{notation_uri}': #{e.}") [] end |
#try_get_notation_results(notation_uri) ⇒ Object
Like get_notation_results but never raises; logs errors and returns [].
458 459 460 461 462 463 |
# File 'lib/keeper_secrets_manager/core.rb', line 458 def try_get_notation_results(notation_uri) get_notation_results(notation_uri) rescue NotationError, RecordNotFoundError, StandardError => e @logger.error("try_get_notation_results failed for '#{notation_uri}': #{e.}") [] end |
#update_folder(folder_uid, folder_name) ⇒ Object
Update folder
549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 |
# File 'lib/keeper_secrets_manager/core.rb', line 549 def update_folder(folder_uid, folder_name) # Get folders to find the folder's key folders = get_folders folder = folders.find { |f| f.uid == folder_uid } raise Error, "Folder #{folder_uid} not found" unless folder folder_key = folder.folder_key raise Error, "Folder key missing for #{folder_uid}" unless folder_key # Prepare folder data folder_data = { 'name' => folder_name } # Encrypt folder data with folder's key using AES-CBC encrypted_data = Crypto.encrypt_aes_cbc( Utils.dict_to_json(folder_data), folder_key ) payload = prepare_update_folder_payload( folder_uid: folder_uid, data: encrypted_data ) post_query('update_folder', payload) true end |
#update_secret(record, transaction_type: 'general') ⇒ Object
Update existing secret (convenience wrapper)
322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 |
# File 'lib/keeper_secrets_manager/core.rb', line 322 def update_secret(record, transaction_type: 'general') = Dto::UpdateOptions.new(transaction_type: transaction_type) (record, ) uid = record.is_a?(Dto::KeeperRecord) ? record.uid : (record['uid'] || record[:uid]) complete_transaction(uid) if uid if record.is_a?(Dto::KeeperRecord) updated_record = get_secrets([record.uid]).first if updated_record record.revision = updated_record.revision @logger&.debug("update_secret: updated local revision to #{record.revision}") end end true end |
#update_secret_with_options(record, update_options = nil) ⇒ Object
Update existing secret with UpdateOptions
288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 |
# File 'lib/keeper_secrets_manager/core.rb', line 288 def (record, = nil) # Handle both record object and hash if record.is_a?(Dto::KeeperRecord) record_uid = record.uid record_data = record.to_h else record_uid = record['uid'] || record[:uid] record_data = record end raise ArgumentError, 'Record UID is required' unless record_uid # Get existing record to get the key existing = get_secrets([record_uid]).first raise RecordNotFoundError, "Record #{record_uid} not found" unless existing # Get record key for encryption record_key = existing.record_key raise Error, "Record key not available for #{record_uid}" unless record_key # Prepare payload (handles UpdateOptions internally) payload = prepare_update_payload( record_uid: record_uid, record_data: record_data, record_key: record_key, revision: existing.revision, update_options: ) # Send request post_query('update_secret', payload) end |
#upload_file(owner_record_uid, file_data, file_name, file_title = nil) ⇒ Object
Upload file
611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 |
# File 'lib/keeper_secrets_manager/core.rb', line 611 def upload_file(owner_record_uid, file_data, file_name, file_title = nil) file_title ||= file_name # Fetch the owner record (decrypted) to get current state owner_records = get_secrets([owner_record_uid]) raise Error, "Owner record #{owner_record_uid} not found" if owner_records.empty? owner_record = owner_records.first owner_revision = owner_record.revision # Get owner record data as hash for manipulation owner_record_data = owner_record.to_h # Get the record_key (stored during decryption) owner_record_key = owner_record.record_key raise Error, "Record key not available for owner record #{owner_record_uid}" unless owner_record_key # Get owner record's public key from storage (app owner public key) owner_public_key = @config.get_string(ConfigKeys::KEY_OWNER_PUBLIC_KEY) raise Error, "Owner public key not found in config - application may need re-binding" unless owner_public_key owner_public_key_bytes = Utils.url_safe_str_to_bytes(owner_public_key) # Generate file record UID and key file_uid = Utils.generate_uid file_key = Crypto.generate_encryption_key_bytes # Encrypt file data with file key encrypted_file = Crypto.encrypt_aes_gcm(file_data, file_key) # Create file record metadata file_record = { 'name' => file_name, 'size' => file_data.bytesize, 'title' => file_title, 'lastModified' => (Time.now.to_f * 1000).to_i, 'type' => 'application/octet-stream' } # Encrypt file record metadata with file key file_record_json = Utils.dict_to_json(file_record) file_record_bytes = file_record_json.bytes encrypted_file_record = Crypto.encrypt_aes_gcm(file_record_bytes.pack('C*'), file_key) # Encrypt file record key with owner's public key (ECIES) encrypted_file_record_key = Crypto.encrypt_ec(file_key, owner_public_key_bytes) # Encrypt file record key with owner record key (for linkKey) encrypted_link_key = Crypto.encrypt_aes_gcm(file_key, owner_record_key) # Add fileRef to owner record's fields fields = owner_record_data['fields'] || [] file_ref_field = fields.find { |f| f['type'] == 'fileRef' } if file_ref_field file_ref_field['value'] ||= [] file_ref_field['value'] << file_uid else fields << { 'type' => 'fileRef', 'value' => [file_uid] } end # Update owner record data owner_record_data['fields'] = fields owner_record_json = Utils.dict_to_json(owner_record_data) owner_record_bytes = owner_record_json.bytes.pack('C*') # Encrypt updated owner record with its record key encrypted_owner_record_data = Crypto.encrypt_aes_gcm(owner_record_bytes, owner_record_key) # Prepare payload payload = prepare_file_upload_payload( file_record_uid: file_uid, file_record_key: encrypted_file_record_key, file_record_data: encrypted_file_record, owner_record_uid: owner_record_uid, owner_record_data: encrypted_owner_record_data, owner_record_revision: owner_revision, link_key: encrypted_link_key, file_size: encrypted_file.bytesize ) # Get upload URL response = post_query('add_file', payload) upload_result = JSON.parse(response) # Upload file upload_file_function( upload_result['url'], upload_result['parameters'], encrypted_file ) file_uid end |
#upload_file_from_path(owner_record_uid, file_path, file_title: nil) ⇒ Object
Upload file from disk path (convenience method) Reads file from disk and uploads to specified record
708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 |
# File 'lib/keeper_secrets_manager/core.rb', line 708 def upload_file_from_path(owner_record_uid, file_path, file_title: nil) raise ArgumentError, "File not found: #{file_path}" unless File.exist?(file_path) raise ArgumentError, "Path is a directory: #{file_path}" if File.directory?(file_path) # Read file data file_data = File.binread(file_path) # Extract filename from path file_name = File.basename(file_path) # Use file_title if provided, otherwise use filename file_title ||= file_name @logger.debug("Uploading file from path: #{file_path} (#{file_data.bytesize} bytes)") # Delegate to existing upload_file method upload_file(owner_record_uid, file_data, file_name, file_title) end |