Class: Llmemory::Crypto::Cipher

Inherits:
Object
  • Object
show all
Defined in:
lib/llmemory/crypto/cipher.rb

Overview

AES-256-GCM encryption with separate content (random IV) and index (deterministic IV) subkeys derived from the master key via HMAC-SHA256.

Constant Summary collapse

MARKER =
"enc:v1:"
DETERMINISTIC_MARKER =
"encd:v1:"
IV_LENGTH =
12
TAG_LENGTH =
16

Instance Method Summary collapse

Constructor Details

#initialize(key) ⇒ Cipher

Returns a new instance of Cipher.



49
50
51
52
53
# File 'lib/llmemory/crypto/cipher.rb', line 49

def initialize(key)
  @master_key = derive_master_key(key)
  @content_key = derive_subkey("content")
  @index_key = derive_subkey("index")
end

Instance Method Details

#decrypt(ciphertext) ⇒ Object



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
# File 'lib/llmemory/crypto/cipher.rb', line 74

def decrypt(ciphertext)
  str = ciphertext.to_s
  return str if str.empty?
  return str unless encrypted?(str)

  marker, key = if str.start_with?(DETERMINISTIC_MARKER)
    [DETERMINISTIC_MARKER, @index_key]
  else
    [MARKER, @content_key]
  end

  payload = decode64(str.delete_prefix(marker))
  iv = payload[0, IV_LENGTH]
  tag = payload[IV_LENGTH, TAG_LENGTH]
  ct = payload[(IV_LENGTH + TAG_LENGTH)..]

  cipher = OpenSSL::Cipher.new("aes-256-gcm")
  cipher.decrypt
  cipher.key = key
  cipher.iv = iv
  cipher.auth_tag = tag
  cipher.auth_data = ""
  cipher.update(ct) + cipher.final
rescue OpenSSL::Cipher::CipherError, ArgumentError => e
  raise DecryptionError, "Failed to decrypt data: #{e.message}"
end

#decrypt_json(str) ⇒ Object



105
106
107
# File 'lib/llmemory/crypto/cipher.rb', line 105

def decrypt_json(str)
  JSON.parse(decrypt(str), symbolize_names: true)
end

#enabled?Boolean

Returns:

  • (Boolean)


55
56
57
# File 'lib/llmemory/crypto/cipher.rb', line 55

def enabled?
  true
end

#encrypt(plaintext) ⇒ Object



59
60
61
62
63
64
# File 'lib/llmemory/crypto/cipher.rb', line 59

def encrypt(plaintext)
  str = plaintext.to_s
  return str if str.empty?

  encrypt_with_key(str, @content_key, iv: OpenSSL::Random.random_bytes(IV_LENGTH), marker: MARKER)
end

#encrypt_deterministic(plaintext) ⇒ Object



66
67
68
69
70
71
72
# File 'lib/llmemory/crypto/cipher.rb', line 66

def encrypt_deterministic(plaintext)
  str = plaintext.to_s
  return str if str.empty?

  iv = OpenSSL::HMAC.digest("SHA256", @index_key, str)[0, IV_LENGTH]
  encrypt_with_key(str, @index_key, iv: iv, marker: DETERMINISTIC_MARKER)
end

#encrypt_json(obj) ⇒ Object



101
102
103
# File 'lib/llmemory/crypto/cipher.rb', line 101

def encrypt_json(obj)
  encrypt(JSON.generate(obj))
end

#encrypted?(str) ⇒ Boolean

Returns:

  • (Boolean)


109
110
111
112
# File 'lib/llmemory/crypto/cipher.rb', line 109

def encrypted?(str)
  s = str.to_s
  s.start_with?(MARKER) || s.start_with?(DETERMINISTIC_MARKER)
end