Module: Seekrit::Crypto
- Defined in:
- lib/seekrit/crypto.rb
Overview
Zero-knowledge read path, byte-compatible with @seekrit/crypto and crates/seekrit-core. Three steps:
1. Recover the service token's P-256 private key (TokenKey.parse).
2. Unwrap each environment DEK (TokenKey#unwrap_dek).
3. Decrypt each secret (Crypto.decrypt_secret).
Blob formats (all segments base64url, no padding):
token skt_<id>_<pkcs8 private key>
wrapped DEK wd1.<ephemeral pub (raw SEC1)>.<hkdf salt>.<iv>.<ciphertext||tag>
secret sc1.<iv>.<ciphertext||tag>
Defined Under Namespace
Classes: TokenKey
Constant Summary collapse
- HKDF_INFO =
"seekrit/wrap-dek/v1"- CURVE =
NIST P-256 / secp256r1
"prime256v1"- TOKEN_RE =
Only the first two underscores are separators; the key segment may contain "_".
/\A(skt_[0-9A-Za-z]+)_([A-Za-z0-9_-]+)\z/.freeze
Class Method Summary collapse
- .aes_gcm_decrypt(key, iv, ciphertext_and_tag, aad) ⇒ Object
- .b64url_decode(text) ⇒ Object
-
.decrypt_secret(dek, blob, aad) ⇒ Object
Decrypt an sc1.
-
.hkdf_sha256(ikm, salt, info, length) ⇒ Object
HKDF-SHA256 (RFC 5869), implemented with HMAC so it works on every Ruby / OpenSSL / LibreSSL build regardless of OpenSSL::KDF.hkdf availability.
-
.materialize(resolve_response, key) ⇒ Object
Decrypt every layer and merge by precedence.
-
.secret_aad(environment_id, name) ⇒ Object
AAD binding a secret ciphertext to its environment and name.
- .split_blob(blob, prefix, segments) ⇒ Object
Class Method Details
.aes_gcm_decrypt(key, iv, ciphertext_and_tag, aad) ⇒ Object
62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 |
# File 'lib/seekrit/crypto.rb', line 62 def aes_gcm_decrypt(key, iv, ciphertext_and_tag, aad) raise CryptoError, "ciphertext too short" if ciphertext_and_tag.bytesize < 16 tag = ciphertext_and_tag[-16, 16] ciphertext = ciphertext_and_tag[0...-16] cipher = OpenSSL::Cipher.new("aes-256-gcm") cipher.decrypt cipher.key = key cipher.iv = iv cipher.auth_tag = tag # GCM with no AAD == empty AAD; skip the call so a zero-length AAD never # trips broken bindings (e.g. some LibreSSL builds). cipher.auth_data = aad unless aad.nil? || aad.empty? cipher.update(ciphertext) + cipher.final end |
.b64url_decode(text) ⇒ Object
26 27 28 |
# File 'lib/seekrit/crypto.rb', line 26 def b64url_decode(text) Base64.urlsafe_decode64(text + ("=" * ((4 - (text.length % 4)) % 4))) end |
.decrypt_secret(dek, blob, aad) ⇒ Object
Decrypt an sc1. secret ciphertext to its UTF-8 plaintext.
132 133 134 135 136 137 138 139 140 141 142 |
# File 'lib/seekrit/crypto.rb', line 132 def decrypt_secret(dek, blob, aad) iv_b64, ct_b64 = split_blob(blob, "sc1", 2) iv = b64url_decode(iv_b64) ct = b64url_decode(ct_b64) plaintext = begin aes_gcm_decrypt(dek, iv, ct, aad) rescue OpenSSL::Cipher::CipherError raise CryptoError, "secret decryption failed: wrong key, tampered data, or mismatched context" end plaintext.force_encoding(Encoding::UTF_8) end |
.hkdf_sha256(ikm, salt, info, length) ⇒ Object
HKDF-SHA256 (RFC 5869), implemented with HMAC so it works on every Ruby / OpenSSL / LibreSSL build regardless of OpenSSL::KDF.hkdf availability.
49 50 51 52 53 54 55 56 57 58 59 60 |
# File 'lib/seekrit/crypto.rb', line 49 def hkdf_sha256(ikm, salt, info, length) prk = OpenSSL::HMAC.digest("SHA256", salt, ikm) okm = +"" block = +"" counter = 0 while okm.bytesize < length counter += 1 block = OpenSSL::HMAC.digest("SHA256", prk, block + info + [counter].pack("C")) okm << block end okm[0, length] end |
.materialize(resolve_response, key) ⇒ Object
Decrypt every layer and merge by precedence. Layers arrive lowest precedence first (composed groups, then the app environment); later layers overwrite earlier ones on a name collision.
147 148 149 150 151 152 153 154 155 156 157 |
# File 'lib/seekrit/crypto.rb', line 147 def materialize(resolve_response, key) merged = {} resolve_response["layers"].each do |layer| dek = key.unwrap_dek(layer["wrappedDek"]) env_id = layer["environmentId"] layer["secrets"].each do |secret| merged[secret["name"]] = decrypt_secret(dek, secret["ciphertext"], secret_aad(env_id, secret["name"])) end end merged end |
.secret_aad(environment_id, name) ⇒ Object
AAD binding a secret ciphertext to its environment and name.
43 44 45 |
# File 'lib/seekrit/crypto.rb', line 43 def secret_aad(environment_id, name) "#{environment_id}/#{name}" end |
.split_blob(blob, prefix, segments) ⇒ Object
30 31 32 33 34 35 36 37 38 39 40 |
# File 'lib/seekrit/crypto.rb', line 30 def split_blob(blob, prefix, segments) parts = blob.split(".") unless parts[0] == prefix raise CryptoError, %(expected a "#{prefix}" blob, got "#{parts[0]}") end if parts.length != segments + 1 || parts.any?(&:empty?) raise CryptoError, %(malformed "#{prefix}" blob) end parts[1..] end |