Returns the 32-byte shared secret that a ciphertext from
kemencap() carries, as hex. This is the
recipient's side of the
exchange: with the secret half of the key pair, the recipient recovers
the same secret the sender kept, and the two can then use it as a common
key.
Usage
kemdecap(key, ct, masked = getOption("rmoriebricklayer.masked", TRUE))Arguments
- key
A key from
kemkg(), with its secret half.- ct
The
ctelement fromkemencap(), as a hex string or a raw vector, of the length for the key's level.- masked
Logical,
TRUEorFALSE.rmoriebricklayer.masked, an option, gives the default when it is set; otherwise the default isTRUE.TRUEruns the first-order masked decapsulation,FALSEthe plain one. Both return the same secret.
Details
This is ML-KEM.Decaps of FIPS 203 (Algorithm 21). For a ciphertext of the right length there is no failure path, and that is deliberate. A ciphertext that was not produced by a correct encapsulation under this key yields a shared secret derived from a value held only inside the decapsulation key – so it is a real secret, just not the sender's. Whoever sent it learns nothing about whether it was accepted, which is what closes off a chosen-ciphertext attack. The consequence for a caller: a mismatch between the two sides' secrets is the signal that something was wrong, not an error from this function.
Masked by default. With masked = TRUE every value that depends on the
secret key is held as two random shares from the moment the key is read
(first-order masking): decryption, the decoding of the message, the
hashing, the re-encryption with its noise sampling, the comparison with
the ciphertext and the choice between the real and the rejection secret.
No single intermediate value then depends on the key, which is what defeats
first-order power and electromagnetic analysis; inst/tvla checks the
masked gadgets against that leakage model. The result is identical to the
unmasked computation for every ciphertext; it costs about 2.3 to 3.4 times
the plain time depending on the CPU (4 ms for ML-KEM-768 on this
package's test machine against 1.2 ms plain). masked = FALSE runs the
plain constant-time decapsulation, and the option
rmoriebricklayer.masked sets the default for a session.
Errors (checked before any decapsulation): key not from
kemkg(), or a public key only (“decapsulation needs a
key with its
secret half”); a ciphertext that is not hex or raw of the exact length
for the key's level, which is
kemsz()[["ciphertext"]] bytes (768,
1088 or 1568); masked not TRUE or FALSE.
Examples
key <- kemkg(512)
sent <- kemencap(kempubkey(key))
identical(kemdecap(key, sent$ciphertext), sent$shared)
#> [1] TRUE
# a tampered ciphertext returns a secret, and it is the wrong one
bad <- sent$ciphertext
substring(bad, 3L, 4L) <- "ff"
other <- kemdecap(key, bad)
nchar(other) == 64L
#> [1] TRUE
identical(other, sent$shared)
#> [1] FALSE
# the masked and the plain decapsulation agree
identical(kemdecap(key, bad, masked = FALSE), other)
#> [1] TRUE
# a public key alone cannot decapsulate, and a short ciphertext is refused
try(kemdecap(kempubkey(key), sent$ciphertext))
#> Error : kemdecap: decapsulation needs a key with its secret half
try(kemdecap(key, "abcd"))
#> Error : `ct` must be 768 bytes for ML-KEM-512
