Class: Threencr::TokenCrypt
- Inherits:
-
Object
- Object
- Threencr::TokenCrypt
- Defined in:
- lib/3ncr.rb
Overview
A 3ncr.org v1 encrypter / decrypter bound to a 32-byte AES key.
Class Method Summary collapse
-
.from_argon2id(secret, salt) ⇒ Object
Derive the AES key from a low-entropy secret via Argon2id using the 3ncr.org v1 recommended parameters (m=19456 KiB, t=2, p=1).
-
.from_raw_key(key) ⇒ Object
Build a TokenCrypt from a raw 32-byte AES-256 key.
-
.from_sha3(secret) ⇒ Object
Derive the AES key from a high-entropy secret via a single SHA3-256 hash.
Instance Method Summary collapse
-
#decrypt_if_3ncr(value) ⇒ Object
Decrypt
valueif it carries the3ncr.org/1#header; otherwise return it unchanged. -
#encrypt_3ncr(plaintext) ⇒ Object
Encrypt a UTF-8 string and return a 3ncr.org/1#… value.
-
#initialize(key) ⇒ TokenCrypt
constructor
A new instance of TokenCrypt.
Constructor Details
#initialize(key) ⇒ TokenCrypt
Returns a new instance of TokenCrypt.
98 99 100 |
# File 'lib/3ncr.rb', line 98 def initialize(key) @key = key end |
Class Method Details
.from_argon2id(secret, salt) ⇒ Object
Derive the AES key from a low-entropy secret via Argon2id using the 3ncr.org v1 recommended parameters (m=19456 KiB, t=2, p=1).
salt must be at least 16 bytes. For deterministic derivation across implementations, pass the same salt.
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 |
# File 'lib/3ncr.rb', line 69 def self.from_argon2id(secret, salt) raise ArgumentError, "salt must be a String" unless salt.is_a?(String) salt_bytes = salt.b if salt_bytes.bytesize < ARGON2ID_MIN_SALT_BYTES raise ArgumentError, "salt must be at least #{ARGON2ID_MIN_SALT_BYTES} bytes, got #{salt_bytes.bytesize}" end secret_bytes = secret.is_a?(String) ? secret.b : String(secret).b # +Argon2id::Password.create+ generates its own random salt, so we drop # to the gem's underlying +hash_encoded+ primitive (private but stable # across releases) to derive with our caller-supplied salt. The encoded # string is then re-parsed via +Password.new+ to extract the raw # +output+ bytes. encoded = Argon2id::Password.send( :hash_encoded, ARGON2ID_TIME_COST, ARGON2ID_MEMORY_KIB, ARGON2ID_PARALLELISM, secret_bytes, salt_bytes, AES_KEY_SIZE ) key = Argon2id::Password.new(encoded).output new(key) end |
.from_raw_key(key) ⇒ Object
Build a TokenCrypt from a raw 32-byte AES-256 key.
Use this when your secret is already high-entropy and exactly 32 bytes (for example, loaded from a key-management service).
41 42 43 44 45 46 47 48 49 50 |
# File 'lib/3ncr.rb', line 41 def self.from_raw_key(key) raise ArgumentError, "key must be a String" unless key.is_a?(String) bytes = key.b unless bytes.bytesize == AES_KEY_SIZE raise ArgumentError, "key must be exactly #{AES_KEY_SIZE} bytes, got #{bytes.bytesize}" end new(bytes) end |
.from_sha3(secret) ⇒ Object
Derive the AES key from a high-entropy secret via a single SHA3-256 hash.
Suitable for random pre-shared keys, UUIDs, or long random API tokens —inputs that already carry at least 128 bits of unique entropy. For low-entropy inputs such as user passwords, prefer from_argon2id.
58 59 60 61 62 |
# File 'lib/3ncr.rb', line 58 def self.from_sha3(secret) bytes = secret.is_a?(String) ? secret.b : String(secret).b key = OpenSSL::Digest.new("SHA3-256").digest(bytes) new(key) end |
Instance Method Details
#decrypt_if_3ncr(value) ⇒ Object
Decrypt value if it carries the 3ncr.org/1# header; otherwise return it unchanged. This makes it safe to route every configuration value through it regardless of whether it was encrypted.
121 122 123 124 125 126 |
# File 'lib/3ncr.rb', line 121 def decrypt_if_3ncr(value) raise ArgumentError, "value must be a String" unless value.is_a?(String) return value unless value.start_with?(HEADER_V1) decrypt(value[HEADER_V1.length..]) end |
#encrypt_3ncr(plaintext) ⇒ Object
Encrypt a UTF-8 string and return a 3ncr.org/1#… value.
103 104 105 106 107 108 109 110 111 112 113 114 115 116 |
# File 'lib/3ncr.rb', line 103 def encrypt_3ncr(plaintext) raise ArgumentError, "plaintext must be a String" unless plaintext.is_a?(String) iv = SecureRandom.bytes(IV_SIZE) cipher = OpenSSL::Cipher.new("aes-256-gcm") cipher.encrypt cipher.key = @key cipher.iv = iv data = plaintext.b # Ruby 3.1's OpenSSL raises on +update("")+; skip it for empty input. ciphertext = data.empty? ? cipher.final : cipher.update(data) + cipher.final payload = iv + ciphertext + cipher.auth_tag(TAG_SIZE) HEADER_V1 + Base64.strict_encode64(payload).delete("=") end |