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

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.message}" 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.message}" 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:)
  timestamp = Time.now.utc.iso8601
  entry = {
    subject_id: subject_id.to_s,
    erased_at:  timestamp,
    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_logObject

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_keyObject



82
83
84
# File 'lib/legion/phi/erasure.rb', line 82

def generate_ephemeral_key
  OpenSSL::Random.random_bytes(32)
end

.generate_key_idObject



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