Module: Legion::Phi::Erasure
- Defined in:
- lib/legion/phi/erasure.rb
Constant Summary collapse
- ERASURE_MARKER =
'[ERASED]'- ERASURE_ALGORITHM =
'aes-256-gcm'
Class Method Summary collapse
- .append_erasure_log(entry) ⇒ Object
- .encrypt_and_erase(value, key, key_id) ⇒ Object
-
.erase_for_subject(subject_id:) ⇒ Object
Erase all PHI for a data subject.
-
.erase_record(record:, phi_fields:, key_id: nil) ⇒ Object
Erase PHI in a single record by encrypting PHI fields with a throwaway key.
-
.erasure_log ⇒ Object
Returns the in-process erasure audit trail.
- .generate_ephemeral_key ⇒ Object
- .generate_key_id ⇒ Object
-
.reset_erasure_log! ⇒ Object
Clears the in-process erasure log (used for testing).
Class Method Details
.append_erasure_log(entry) ⇒ Object
90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 |
# File 'lib/legion/phi/erasure.rb', line 90 def append_erasure_log(entry) @erasure_log ||= [] @erasure_log << entry if defined?(Legion::Audit) Legion::Audit.record( event_type: 'phi_erasure', principal_id: entry[:subject_id], action: 'erase', resource: "subject/#{entry[:subject_id]}", source: 'phi_erasure', detail: "method=#{entry[:method]};algorithm=#{entry[:algorithm]};key_id=#{entry[:key_id]}" ) elsif defined?(Legion::Logging) Legion::Logging.info( "[PHI ERASURE] subject=#{entry[:subject_id]} method=#{entry[:method]} " \ "algorithm=#{entry[:algorithm]} at=#{entry[:erased_at]}" ) end rescue StandardError => e # Never raise from erasure log — ensure the erase always appears to succeed Legion::Logging.warn "Phi::Erasure#append_erasure_log failed for subject=#{entry[:subject_id]}: #{e.}" if defined?(Legion::Logging) end |
.encrypt_and_erase(value, key, key_id) ⇒ Object
62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 |
# File 'lib/legion/phi/erasure.rb', line 62 def encrypt_and_erase(value, key, key_id) return ERASURE_MARKER if value.nil? plaintext = value.to_s cipher = OpenSSL::Cipher.new(ERASURE_ALGORITHM) cipher.encrypt cipher.key = key[0, 32] iv = cipher.random_iv cipher.iv = iv ciphertext = cipher.update(plaintext) + cipher.final tag = cipher.auth_tag # Return an erasure marker with minimal forensic metadata (no recoverable data) "#{ERASURE_MARKER}[key_id=#{key_id},iv=#{iv.unpack1('H*')},tag=#{tag.unpack1('H*')},len=#{ciphertext.bytesize}]" rescue OpenSSL::Cipher::CipherError => e Legion::Logging.warn "Phi::Erasure#encrypt_and_erase cipher error for key_id=#{key_id}: #{e.}" if defined?(Legion::Logging) ERASURE_MARKER end |
.erase_for_subject(subject_id:) ⇒ Object
Erase all PHI for a data subject. Returns an erasure audit entry.
14 15 16 17 18 19 20 21 22 23 24 25 26 |
# File 'lib/legion/phi/erasure.rb', line 14 def erase_for_subject(subject_id:) = Time.now.utc.iso8601 entry = { subject_id: subject_id.to_s, erased_at: , method: 'cryptographic_erasure', algorithm: ERASURE_ALGORITHM, key_id: generate_key_id, status: 'completed' } append_erasure_log(entry) entry end |
.erase_record(record:, phi_fields:, key_id: nil) ⇒ Object
Erase PHI in a single record by encrypting PHI fields with a throwaway key. The key is immediately discarded, making the data unrecoverable.
30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
# File 'lib/legion/phi/erasure.rb', line 30 def erase_record(record:, phi_fields:, key_id: nil) return record unless record.is_a?(Hash) return record if phi_fields.nil? || phi_fields.empty? key_id ||= generate_key_id ephemeral_key = generate_ephemeral_key result = record.dup phi_fields.each do |field| next unless result.key?(field) result[field] = encrypt_and_erase(result[field], ephemeral_key, key_id) end # Destroy the ephemeral key immediately — data is now unrecoverable ephemeral_key.replace(OpenSSL::Random.random_bytes(32)) ephemeral_key = nil result end |
.erasure_log ⇒ Object
Returns the in-process erasure audit trail.
52 53 54 55 |
# File 'lib/legion/phi/erasure.rb', line 52 def erasure_log @erasure_log ||= [] @erasure_log.dup.freeze end |
.generate_ephemeral_key ⇒ Object
82 83 84 |
# File 'lib/legion/phi/erasure.rb', line 82 def generate_ephemeral_key OpenSSL::Random.random_bytes(32) end |
.generate_key_id ⇒ Object
86 87 88 |
# File 'lib/legion/phi/erasure.rb', line 86 def generate_key_id OpenSSL::Random.random_bytes(16).unpack1('H*') end |
.reset_erasure_log! ⇒ Object
Clears the in-process erasure log (used for testing).
58 59 60 |
# File 'lib/legion/phi/erasure.rb', line 58 def reset_erasure_log! @erasure_log = [] end |