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
- 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
-
#create_folder(folder_name, parent_uid: nil) ⇒ Object
Create folder.
-
#create_secret(record_data, options = nil) ⇒ Object
Create a new secret.
-
#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.
-
#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_notation(notation_uri) ⇒ Object
Get notation value.
-
#get_secret_by_title(title) ⇒ Object
Get first secret by title.
-
#get_secrets(uids = nil, full_response: 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.
-
#initialize(options = {}) ⇒ SecretsManager
constructor
A new instance of SecretsManager.
-
#update_folder(folder_uid, folder_name) ⇒ Object
Update folder.
-
#update_secret(record, transaction_type: 'general') ⇒ Object
Update existing secret.
-
#upload_file(owner_record_uid, file_data, file_name, file_title = nil) ⇒ Object
Upload file.
Constructor Details
#initialize(options = {}) ⇒ SecretsManager
Returns a new instance of SecretsManager.
21 22 23 24 25 26 27 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 |
# File 'lib/keeper_secrets_manager/core.rb', line 21 def initialize( = {}) # Check Ruby version raise Error, 'KSM SDK requires Ruby 2.6 or greater' if RUBY_VERSION < '2.6' # 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] # Set up logging @logger = [:logger] || Logger.new(STDOUT) @logger.level = [:log_level] || Logger::WARN # 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 # 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 # Cache configuration @cache = {} @cache_expiry = {} 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
#create_folder(folder_name, parent_uid: nil) ⇒ Object
Create folder
354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 |
# File 'lib/keeper_secrets_manager/core.rb', line 354 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
Create a new secret
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 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 |
# File 'lib/keeper_secrets_manager/core.rb', line 206 def create_secret(record_data, = nil) ||= Dto::CreateOptions.new # Validate folder UID is provided raise ArgumentError, 'folder_uid is required to create a record' unless .folder_uid # Get folders from dedicated endpoint to find folder key folders = get_folders # Find the folder folder = folders.find { |f| f.uid == .folder_uid } raise Error, "Folder #{options.folder_uid} not found or not accessible" unless folder # Get folder key folder_key = folder.folder_key raise Error, "Unable to create record - folder key for #{options.folder_uid} is missing" unless folder_key # Generate UIDs and keys record_uid = Utils.generate_uid record_key = Crypto.generate_encryption_key_bytes # Prepare record data record = if record_data.is_a?(Dto::KeeperRecord) record_data.to_h else record_data end # Encrypt record data encrypted_data = Crypto.encrypt_aes_gcm( Utils.dict_to_json(record), record_key ) # Prepare payload payload = prepare_create_payload( record_uid: record_uid, record_key: record_key, folder_uid: .folder_uid, folder_key: folder_key, data: encrypted_data ) # Send request response = post_query('create_secret', payload) # Return created record UID record_uid end |
#delete_folder(folder_uids, force: false) ⇒ Object
Delete folders
457 458 459 460 461 462 463 464 465 |
# File 'lib/keeper_secrets_manager/core.rb', line 457 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) result['folders'] end |
#delete_secret(record_uids) ⇒ Object
Delete secrets
337 338 339 340 341 342 343 344 345 |
# File 'lib/keeper_secrets_manager/core.rb', line 337 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) result['records'] end |
#download_encrypted_file(url) ⇒ Object
Download encrypted file from URL
640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 |
# File 'lib/keeper_secrets_manager/core.rb', line 640 def download_encrypted_file(url) uri = URI(url) @logger.debug("Downloading file from URL: #{url}") request = Net::HTTP::Get.new(uri) http = Net::HTTP.new(uri.host, uri.port) 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.message}" end end |
#download_file(file_data) ⇒ Object
Download file from record’s file data
580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 |
# File 'lib/keeper_secrets_manager/core.rb', line 580 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 |
#fetch_and_decrypt_folders ⇒ Object
Fetch and decrypt folders from dedicated endpoint
123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 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 |
# File 'lib/keeper_secrets_manager/core.rb', line 123 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.message}") end folders end |
#find_folder_by_name(name, parent_uid: nil) ⇒ Object
Find folder by name (convenience method)
479 480 481 |
# File 'lib/keeper_secrets_manager/core.rb', line 479 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
468 469 470 471 |
# File 'lib/keeper_secrets_manager/core.rb', line 468 def folder_manager folders = get_folders FolderManager.new(folders) end |
#get_file_data(file_uid) ⇒ Object
Get file metadata from server
608 609 610 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 |
# File 'lib/keeper_secrets_manager/core.rb', line 608 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)
474 475 476 |
# File 'lib/keeper_secrets_manager/core.rb', line 474 def get_folder_path(folder_uid) folder_manager.get_folder_path(folder_uid) end |
#get_folders ⇒ Object
Get all folders
118 119 120 |
# File 'lib/keeper_secrets_manager/core.rb', line 118 def get_folders fetch_and_decrypt_folders end |
#get_notation(notation_uri) ⇒ Object
Get notation value
348 349 350 351 |
# File 'lib/keeper_secrets_manager/core.rb', line 348 def get_notation(notation_uri) parser = Notation::Parser.new(self) parser.parse(notation_uri) end |
#get_secret_by_title(title) ⇒ Object
Get first secret by title
201 202 203 |
# File 'lib/keeper_secrets_manager/core.rb', line 201 def get_secret_by_title(title) get_secrets_by_title(title).first end |
#get_secrets(uids = nil, full_response: false) ⇒ Object
Get secrets with optional filtering
90 91 92 93 94 95 |
# File 'lib/keeper_secrets_manager/core.rb', line 90 def get_secrets(uids = nil, full_response: false) uids = [uids] if uids.is_a?(String) = Dto::QueryOptions.new(records: uids, folders: nil) (, full_response: full_response) end |
#get_secrets_by_title(title) ⇒ Object
Get secrets by title
195 196 197 198 |
# File 'lib/keeper_secrets_manager/core.rb', line 195 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
98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 |
# File 'lib/keeper_secrets_manager/core.rb', line 98 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 |
#update_folder(folder_uid, folder_name) ⇒ Object
Update folder
427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 |
# File 'lib/keeper_secrets_manager/core.rb', line 427 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
257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 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 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 |
# File 'lib/keeper_secrets_manager/core.rb', line 257 def update_secret(record, transaction_type: 'general') # 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 # Record key is already raw bytes (stored during decryption) # No conversion needed - use directly for encryption # Debug: Log record data before encryption @logger&.debug("update_secret: record_uid=#{record_uid}") @logger&.debug("update_secret: record_data keys=#{record_data.keys.inspect}") @logger&.debug("update_secret: record_data=#{record_data.inspect[0..200]}...") @logger&.debug("update_secret: record_key present=#{!record_key.nil?}, length=#{record_key&.bytesize}") # Encrypt record data with record key (same as create_secret) json_data = Utils.dict_to_json(record_data) @logger&.debug("update_secret: json_data length=#{json_data.bytesize}") @logger&.debug("update_secret: json_data=#{json_data[0..200]}...") encrypted_data = Crypto.encrypt_aes_gcm(json_data, record_key) @logger&.debug("update_secret: encrypted_data length=#{encrypted_data.bytesize}") @logger&.debug("update_secret: encrypted_data (base64)=#{Base64.strict_encode64(encrypted_data)[0..50]}...") # Prepare payload payload = prepare_update_payload( record_uid: record_uid, data: encrypted_data, revision: existing.revision, transaction_type: transaction_type ) @logger&.debug("update_secret: payload revision=#{existing.revision}") @logger&.debug("update_secret: payload transaction_type=#{transaction_type}") # Send request @logger&.debug("update_secret: sending post_query to update_secret endpoint") response = post_query('update_secret', payload) @logger&.debug("update_secret: response received") @logger&.debug("update_secret: response class=#{response.class}") @logger&.debug("update_secret: response=#{response.inspect[0..500]}...") # Always finalize the update (required for changes to persist) # This applies to both 'general' and 'rotation' transaction types complete_payload = Dto::CompleteTransactionPayload.new complete_payload.client_version = KeeperGlobals.client_version complete_payload.client_id = @config.get_string(ConfigKeys::KEY_CLIENT_ID) complete_payload.record_uid = record_uid post_query('finalize_secret_update', complete_payload) # Update local record's revision to reflect server state # Since the server doesn't return the new revision in the response, # we need to refetch the record to get the actual revision 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 |
#upload_file(owner_record_uid, file_data, file_name, file_title = nil) ⇒ Object
Upload file
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 547 548 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 577 |
# File 'lib/keeper_secrets_manager/core.rb', line 484 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 |