///|
/// Encrypt plaintext to a passphrase. Produces an armored PGP MESSAGE with
/// a v4 SKESK (iterated+salted S2K, AES-256 session key wrapped in CFB)
/// followed by a v1 SEIPD packet holding the literal data.
pub fn encrypt_with_password(
plaintext : BytesView,
passphrase : String,
file_name? : String,
aead~ : Bool = false,
argon2~ : Bool = false,
argon2_memory_exp~ : Int32 = 16,
compress? : @compress.CompressedAlgo,
) -> String raise {
if aead {
return seal_aead_with_password(
plaintext,
passphrase,
file_name?,
argon2~,
argon2_memory_exp~,
compress?,
)
}
// `argon2` selects the S2K specifier (RFC 9580 §3.7.1.4) and is
// orthogonal to the SKESK/SEIPD versions: the v4 SKESK form carries any
// specifier. Ignoring it here (and using iterated SHA-256) would be a
// silent KDF downgrade.
let s2k : @s2k.S2k = if argon2 {
let salt = @primitives.random_bytes(@s2k.ARGON2_SALT_SIZE)
@s2k.Argon2(salt, 3, 4, argon2_memory_exp)
} else {
let salt = @primitives.random_bytes(@s2k.SALT_SIZE)
@s2k.Iterated(@primitives.Sha256, salt, @s2k.encode_count(S2K_COUNT_TARGET))
}
seal_skesk_with_password(plaintext, passphrase, s2k, file_name?, compress?)
}
///|
/// Encrypt plaintext to a passphrase using Argon2id as the S2K specifier
/// (type 4, RFC 9580 §3.7.1.4) — the RFC's recommendation, and the shape of
/// Appendix A.12. The message keeps the v4 SKESK form (CFB with an
/// all-zero IV) plus a v1 SEIPD packet, so every reader that understands
/// Argon2 can consume it.
///
/// `memory_exp` is the exponent of the Argon2 memory size in KiB (default
/// 16 → 64 MiB, go-crypto's default); `passes` and `parallelism` default to
/// t=3, p=4.
pub fn encrypt_with_password_argon2(
plaintext : BytesView,
passphrase : String,
file_name? : String,
passes~ : Int32 = 3,
parallelism~ : Int32 = 4,
memory_exp~ : Int32 = 16,
compress? : @compress.CompressedAlgo,
) -> String raise {
let salt = @primitives.random_bytes(@s2k.ARGON2_SALT_SIZE)
let s2k : @s2k.S2k = @s2k.Argon2(salt, passes, parallelism, memory_exp)
seal_skesk_with_password(plaintext, passphrase, s2k, file_name?, compress?)
}
///|
/// Encrypt plaintext to a passphrase in the RFC 9580 v6 shape: a v6 SKESK
/// (AEAD + HKDF-SHA256 key separation, §5.3.2) followed by a v2 SEIPD
/// packet (§5.13.2). `argon2` (default true) selects the Argon2id S2K the
/// RFC recommends for v6; set it to false for the much cheaper
/// iterated+salted SHA-256.
pub fn encrypt_with_password_v6(
plaintext : BytesView,
passphrase : String,
file_name? : String,
aead~ : @cipher.AeadMode = @cipher.Ocb,
argon2~ : Bool = true,
passes~ : Int32 = 3,
parallelism~ : Int32 = 4,
memory_exp~ : Int32 = 16,
compress? : @compress.CompressedAlgo,
) -> String raise {
let algo = @primitives.Aes256
let s2k : @s2k.S2k = if argon2 {
@s2k.Argon2(
@primitives.random_bytes(@s2k.ARGON2_SALT_SIZE),
passes,
parallelism,
memory_exp,
)
} else {
@s2k.Iterated(
@primitives.Sha256,
@primitives.random_bytes(@s2k.SALT_SIZE),
@s2k.encode_count(S2K_COUNT_TARGET),
)
}
let session_key = @primitives.random_bytes(SESSION_KEY_SIZE)
let literal = @packet.LiteralPacket::binary(plaintext, file_name?)
let literal_wire = @packet.serialize_packet(
@packet.Literal,
literal.to_body().exact_view(),
)
let seipd = seal_seipd_v2(
algo,
aead,
SEIPD_V2_CHUNK_OCTET,
session_key,
maybe_compress_inner(literal_wire, compress?).exact_view(),
)
let cipher_algo = algo.number()
let aead_algo = aead.number()
let ad = Bytes::from_array([
0xC3,
6,
cipher_algo.to_byte(),
aead_algo.to_byte(),
])
let kek = s2k.derive_key(
@primitives.utf8_bytes(passphrase).exact_view(),
algo.key_size(),
)
let dek = @primitives.hkdf_sha256(kek, Bytes::new(0), ad, algo.key_size())
let bc = @primitives.new_block_cipher(algo, dek.exact_view())
let nonce = @primitives.random_bytes(aead.iv_length())
let esk = aead_seal_session_key(
bc,
aead,
nonce.exact_view(),
ad.exact_view(),
session_key.exact_view(),
)
let skesk : @packet.SkeskPacket = {
version: 6,
cipher_algo,
aead_algo,
s2k,
iv: nonce,
encrypted_session_key: esk,
}
let w = @packet.Writer::new()
w.bytes(
@packet.serialize_packet(@packet.Skesk, skesk.to_body().exact_view()).exact_view(),
)
w.bytes(
@packet.serialize_packet(@packet.Seipd, seipd.to_body().exact_view()).exact_view(),
)
@armor.Message.encode(w.to_bytes().exact_view())
}
///|
/// Wrap `session_key` with `s2k` into a v4 SKESK and seal the literal data
/// with a v1 SEIPD packet. Shared by every passphrase-encryption entry
/// point; the caller supplies the already-parameterised S2K.
fn seal_skesk_with_password(
plaintext : BytesView,
passphrase : String,
s2k : @s2k.S2k,
file_name? : String,
compress? : @compress.CompressedAlgo,
) -> String raise {
let session_key = @primitives.random_bytes(SESSION_KEY_SIZE)
let skesk = build_skesk(session_key.exact_view(), passphrase, s2k)
let literal = @packet.LiteralPacket::binary(plaintext, file_name?)
let literal_wire = @packet.serialize_packet(
@packet.Literal,
literal.to_body().exact_view(),
)
let seipd_ciphertext = seal_seipd(
session_key.exact_view(),
maybe_compress_inner(literal_wire, compress?).exact_view(),
)
let w = @packet.Writer::new()
w.u8(0xC0 | @packet.Skesk.number())
let skesk_body = skesk.to_body()
write_packet_length(w, skesk_body.length())
w.bytes(skesk_body.exact_view())
w.u8(0xC0 | @packet.Seipd.number())
write_packet_length(w, seipd_ciphertext.length() + 1)
w.u8(1)
w.bytes(seipd_ciphertext.exact_view())
@armor.Message.encode(w.to_bytes().exact_view())
}
///|
/// Encrypt to SEVERAL passphrases (N4): one fresh session key sealed by
/// one SKESK per passphrase (each with its own salt), then a single v1
/// SEIPD over the literal data — the multi-recipient passphrase shape
/// `unwrap_skesk` expects when it walks the SKESKs in turn.
fn seal_skesks_with_passwords(
plaintext : BytesView,
passphrases : Array[String],
file_name? : String,
compress? : @compress.CompressedAlgo,
) -> String raise {
guard !passphrases.is_empty() else {
@error.fail("api: encryption requires at least one passphrase")
}
let session_key = @primitives.random_bytes(SESSION_KEY_SIZE)
let literal = @packet.LiteralPacket::binary(plaintext, file_name?)
let literal_wire = @packet.serialize_packet(
@packet.Literal,
literal.to_body().exact_view(),
)
let seipd_ciphertext = seal_seipd(
session_key.exact_view(),
maybe_compress_inner(literal_wire, compress?).exact_view(),
)
let w = @packet.Writer::new()
for passphrase in passphrases {
let s2k : @s2k.S2k = @s2k.Iterated(
@primitives.Sha256,
@primitives.random_bytes(@s2k.SALT_SIZE),
@s2k.encode_count(S2K_COUNT_TARGET),
)
let skesk = build_skesk(session_key.exact_view(), passphrase, s2k)
w.bytes(
@packet.serialize_packet(@packet.Skesk, skesk.to_body().exact_view()).exact_view(),
)
}
w.bytes(
@packet.serialize_packet(
@packet.Seipd,
Bytes::from_array([1]).add(seipd_ciphertext).exact_view(),
).exact_view(),
)
@armor.Message.encode(w.to_bytes().exact_view())
}
///|
/// Decrypt an (optionally armored) message using a passphrase. Returns the
/// literal data. Compressed packets are transparently decompressed.
pub fn decrypt_with_password(
message : String,
passphrase : String,
argon2_max_memory_kib~ : Int32 = @primitives.DEFAULT_ARGON2_MAX_MEMORY_KIB,
max_decompressed~ : Int32 = DEFAULT_MAX_DECOMPRESSED_BYTES,
) -> Bytes raise {
let armored = @armor.decode(message)
decrypt_binary_with_password(
@armor.ArmorBlock::data(armored),
passphrase,
argon2_max_memory_kib~,
max_decompressed~,
)
}
///|
/// Same as `decrypt_with_password` but takes binary packet data.
///
/// Every SKESK in the stream is tried until one both unwraps AND opens the
/// message payload (N4): a v4 SKESK has no integrity check of its own, so
/// a wrong passphrase can unwrap to structurally plausible garbage whose
/// wrongness only surfaces at the SEIPD MDC — the retry must therefore
/// span the payload decryption, not just the unwrap.
pub fn decrypt_binary_with_password(
data : BytesView,
passphrase : String,
argon2_max_memory_kib~ : Int32 = @primitives.DEFAULT_ARGON2_MAX_MEMORY_KIB,
max_decompressed~ : Int32 = DEFAULT_MAX_DECOMPRESSED_BYTES,
) -> Bytes raise {
let packets = @packet.parse_packets(data)
let mut last_error : String? = None
for p in packets {
if p.tag() == @packet.Skesk {
let skesk = @packet.SkeskPacket::parse(p.body().exact_view())
let opened : Bytes? = try {
let (msg_algo, session_key, skesk_version) = unwrap_one_skesk(
skesk, passphrase, argon2_max_memory_kib,
)
Some(
open_password_payload(
packets, msg_algo, session_key, skesk_version, max_decompressed,
),
)
} catch {
e => {
last_error = Some(e.to_string())
None
}
}
if opened is Some(plain) {
return plain
}
}
}
match last_error {
Some(msg) =>
@error.fail("api: no SKESK in the message matched the passphrase: \{msg}")
None => @error.fail("api: no SKESK packet found")
}
}
///|
/// Open the encrypted payload of a password message with one unwrapped
/// session key: the tag-20 4880bis AEAD container, or the v1/v2 SEIPD
/// whose version must align with the SKESK's (Table 26).
fn open_password_payload(
packets : Array[@packet.RawPacket],
msg_algo : @primitives.CipherAlgo,
session_key : Bytes,
skesk_version : Int32,
max_decompressed : Int32,
) -> Bytes raise {
for p in packets {
if p.tag() == @packet.AeadEncData {
let aead = @packet.AeadPacket::parse(p.body().exact_view())
let algo = match @primitives.cipher_algo_of_number(aead.cipher_algo()) {
Some(a) => a
None =>
@error.fail(
"api: unsupported AEAD cipher algorithm \{aead.cipher_algo()}",
)
}
let mode = match @cipher.aead_mode_of_number(aead.aead_algo()) {
Some(m) => m
None => @error.fail("api: unknown AEAD algorithm \{aead.aead_algo()}")
}
guard session_key.length() == algo.key_size() else {
@error.fail("api: session key length does not match the AEAD cipher")
}
let bc = @primitives.new_block_cipher(algo, session_key.exact_view())
// Associated-data header: 0xD4 | version | cipher | aead | chunk octet.
let header5 = Bytes::from_array([
0xD4,
aead.version().to_byte(),
aead.cipher_algo().to_byte(),
aead.aead_algo().to_byte(),
aead.chunk_octet().to_byte(),
])
let inner = @cipher.aead_open(
bc,
mode,
header5.exact_view(),
aead.iv().exact_view(),
aead.chunk_octet(),
aead.ciphertext().exact_view(),
)
return extract_literal(inner, max_decompressed)
}
if p.tag() == @packet.Seipd {
let seipd = @packet.SeipdPacket::parse(p.body().exact_view())
// RFC 9580 §10.3.2.1 (Table 26): a v4 SKESK pairs with a v1 SEIPD and
// a v6 SKESK with a v2 SEIPD; a mismatched ESK version must be
// discarded. (The tag-20 4880bis AEAD container above is not part of
// RFC 9580 and keeps its lenient handling for GnuPG interop.)
guard (seipd.version == 2) == (skesk_version == 6) else {
@error.fail(
"api: SKESK version \{skesk_version} does not align with SEIPD version \{seipd.version}",
)
}
if seipd.version == 2 {
return decrypt_seipd_v2(seipd, session_key, max_decompressed)
}
guard seipd.version == 1 else {
@error.fail("api: unsupported SEIPD version \{seipd.version}")
}
// The SEIPD cipher is the one named inside the unwrapped session key,
// which need not match the SKESK's wrapping cipher (RFC 9580 §5.3.1).
guard session_key.length() == msg_algo.key_size() else {
@error.fail("api: session key length does not match the message cipher")
}
let bc = @primitives.new_block_cipher(msg_algo, session_key.exact_view())
let inner = @cipher.seipd_v1_decrypt(bc, seipd.ciphertext.exact_view())
return extract_literal(inner, max_decompressed)
}
}
@error.fail("api: no encrypted data packet found")
}
///|
const SESSION_KEY_SIZE : Int32 = 32
///|
/// S2K hashed-material target for passphrase derivation: 56,623,104 octets
/// (~54 MiB), gpg's default count (byte 251). The previous 1 MiB was
/// ~50x lighter than gpg's own interactive cost.
const S2K_COUNT_TARGET : Int32 = 56623104
///|
fn write_packet_length(w : @packet.Writer, length : Int32) -> Unit {
if length < 192 {
w.u8(length)
} else if length < 8384 {
let enc = length - 192 + 192 * 256
w.u8(enc >> 8)
w.u8(enc)
} else {
w.u8(255)
w.u32(length)
}
}
///|
/// Build a v4 SKESK whose S2K-derived key wraps `[algo || session key]` with
/// CFB under an all-zero IV (RFC 9580 §5.3.1: an all-zero IV requires a
/// salted or Argon2 specifier).
fn build_skesk(
session_key : BytesView,
passphrase : String,
s2k : @s2k.S2k,
) -> @packet.SkeskPacket raise {
// The S2K key wraps [algo byte || session key] with CFB, zero IV.
let wrapping_key = s2k.derive_key(
@primitives.utf8_bytes(passphrase).exact_view(),
SESSION_KEY_SIZE,
)
let bc = @primitives.new_block_cipher(
@primitives.Aes256,
wrapping_key.exact_view(),
)
let plain = Bytes::from_array([@primitives.Aes256.number().to_byte()]).add(
session_key.to_owned(),
)
let esk = @cipher.cfb_encrypt(
bc,
Bytes::make(16, b'\x00').exact_view(),
plain.exact_view(),
)
@packet.SkeskPacket::{
version: 4,
cipher_algo: @primitives.Aes256.number(),
aead_algo: 0,
s2k,
iv: Bytes::new(0),
encrypted_session_key: esk,
}
}
///|
/// Recover the session key and its message cipher algorithm from the
/// packet stream's SKESK. Every SKESK in the stream is tried in order
/// until one yields to `passphrase` (N4): a message sealed to several
/// passphrases carries one SKESK per passphrase, and the matching one
/// need not be first. (A v4 SKESK whose wrong-passphrase garbage happens
/// to decode as a valid cipher octet still slips through to fail at the
/// SEIPD integrity check — it has no checksum of its own.)
fn unwrap_skesk(
packets : Array[@packet.RawPacket],
passphrase : String,
argon2_max_memory_kib : Int32,
) -> (@primitives.CipherAlgo, Bytes, Int32) raise {
let mut last_error : String? = None
for p in packets {
if p.tag() == @packet.Skesk {
let skesk = @packet.SkeskPacket::parse(p.body().exact_view())
let unwrapped : (@primitives.CipherAlgo, Bytes, Int32)? = Some(
unwrap_one_skesk(skesk, passphrase, argon2_max_memory_kib),
) catch {
e => {
last_error = Some(e.to_string())
None
}
}
if unwrapped is Some(result) {
return result
}
}
}
match last_error {
Some(msg) =>
@error.fail("api: no SKESK in the message matched the passphrase: \{msg}")
None => @error.fail("api: no SKESK packet found")
}
}
///|
/// Unwrap a single SKESK. Any raise means "this SKESK does not match the
/// passphrase", which the caller treats as a reason to try the next one.
fn unwrap_one_skesk(
skesk : @packet.SkeskPacket,
passphrase : String,
argon2_max_memory_kib : Int32,
) -> (@primitives.CipherAlgo, Bytes, Int32) raise {
let algo = match @primitives.cipher_algo_of_number(skesk.cipher_algo()) {
Some(a) => a
None =>
@error.fail("api: unsupported cipher algorithm \{skesk.cipher_algo()}")
}
let pass = @primitives.utf8_bytes(passphrase)
if skesk.version() == 6 {
// RFC 9580 §5.3.2: the same HKDF-SHA256 key separation as a
// usage-253 secret key, with info/AD = tag | version | cipher |
// aead, and the session key sealed alone (no algorithm octet).
let kek = skesk
.s2k()
.derive_key_capped(
pass.exact_view(),
algo.key_size(),
argon2_max_memory_kib,
)
let mode = match @cipher.aead_mode_of_number(skesk.aead_algo()) {
Some(m) => m
None =>
@error.fail("api: unknown SKESK AEAD algorithm \{skesk.aead_algo()}")
}
let ad = Bytes::from_array([
0xC3,
6,
skesk.cipher_algo().to_byte(),
skesk.aead_algo().to_byte(),
])
let dek = @primitives.hkdf_sha256(kek, Bytes::new(0), ad, algo.key_size())
let bc = @primitives.new_block_cipher(algo, dek.exact_view())
let key = aead_open_session_key(
bc,
mode,
skesk.iv().exact_view(),
ad.exact_view(),
skesk.encrypted_session_key().exact_view(),
)
guard key.length() == algo.key_size() else {
@error.fail("api: AEAD-wrapped session key has the wrong length")
}
return (algo, key, skesk.version())
}
if skesk.version() == 5 {
// v5: the S2K-derived key is the AEAD key directly (no HKDF — the
// HKDF step belongs to the v6 SKESK); AD = C3 05 cipher aead.
let kek = skesk
.s2k()
.derive_key_capped(
pass.exact_view(),
algo.key_size(),
argon2_max_memory_kib,
)
let mode = match @cipher.aead_mode_of_number(skesk.aead_algo()) {
Some(m) => m
None =>
@error.fail("api: unknown SKESK AEAD algorithm \{skesk.aead_algo()}")
}
let bc = @primitives.new_block_cipher(algo, kek.exact_view())
let ad = Bytes::from_array([
0xC3,
5,
skesk.cipher_algo().to_byte(),
skesk.aead_algo().to_byte(),
])
let key = aead_open_session_key(
bc,
mode,
skesk.iv().exact_view(),
ad.exact_view(),
skesk.encrypted_session_key().exact_view(),
)
guard key.length() == algo.key_size() else {
@error.fail("api: AEAD-wrapped session key has the wrong length")
}
return (algo, key, skesk.version())
}
if skesk.encrypted_session_key().length() == 0 {
return (
algo,
skesk
.s2k()
.derive_key_capped(
pass.exact_view(),
algo.key_size(),
argon2_max_memory_kib,
),
skesk.version(),
)
}
let bc = @primitives.new_block_cipher(
algo,
skesk
.s2k()
.derive_key_capped(
pass.exact_view(),
algo.key_size(),
argon2_max_memory_kib,
)
.exact_view(),
)
let wrapped = @cipher.cfb_decrypt(
bc,
Bytes::make(16, b'\x00').exact_view(),
skesk.encrypted_session_key().exact_view(),
)
guard wrapped.length() >= 1 else { @error.fail("api: empty session key") }
// The wrapped value carries the message cipher algorithm octet, which
// may differ from the SKESK's own wrapping cipher (RFC 9580 §5.3.1).
let inner_algo = match
@primitives.cipher_algo_of_number(wrapped[0].to_int()) {
Some(a) => a
None => @error.fail("api: unsupported inner cipher algorithm")
}
guard wrapped.length() - 1 == inner_algo.key_size() else {
@error.fail("api: session key length mismatch")
}
return (inner_algo, wrapped[1:wrapped.length()].to_owned(), skesk.version())
}
///|
fn seal_seipd(session_key : BytesView, inner : BytesView) -> Bytes raise {
let bc = @primitives.new_block_cipher(
@primitives.Aes256,
session_key.exact_view(),
)
@cipher.seipd_v1_encrypt(bc, inner)
}
///|
/// AEAD message builder (the --force-aead shape GnuPG 2.4 consumes):
/// a v5 SKESK holding the OCB-sealed session key, then a tag-20 AEAD
/// packet over the literal data. AES-256 + OCB, chunk octet 14 (1 MiB).
fn seal_aead_with_password(
plaintext : BytesView,
passphrase : String,
file_name? : String,
argon2~ : Bool = false,
argon2_memory_exp~ : Int32 = 16,
compress? : @compress.CompressedAlgo,
) -> String raise {
let cipher_algo = @primitives.Aes256.number()
let aead_algo = @cipher.Ocb.number()
let session_key = @primitives.random_bytes(SESSION_KEY_SIZE)
let s2k : @s2k.S2k = if argon2 {
@s2k.Argon2(
@primitives.random_bytes(@s2k.ARGON2_SALT_SIZE),
3,
4,
argon2_memory_exp,
)
} else {
let salt = @primitives.random_bytes(@s2k.SALT_SIZE)
let count_byte = @s2k.encode_count(S2K_COUNT_TARGET)
@s2k.Iterated(@primitives.Sha256, salt, count_byte)
}
let pass = @primitives.utf8_bytes(passphrase)
let kek = s2k.derive_key(pass.exact_view(), SESSION_KEY_SIZE)
let kek_bc = @primitives.new_block_cipher(
@primitives.Aes256,
kek.exact_view(),
)
let esk_ad = Bytes::from_array([
0xC3,
5,
cipher_algo.to_byte(),
aead_algo.to_byte(),
])
let esk_iv = @primitives.random_bytes(15)
let esk = @cipher.ocb_encrypt(
kek_bc,
esk_iv.exact_view(),
esk_ad.exact_view(),
session_key.exact_view(),
)
let skesk : @packet.SkeskPacket = {
version: 5,
cipher_algo,
aead_algo,
s2k,
iv: esk_iv,
encrypted_session_key: esk,
}
let literal = @packet.LiteralPacket::binary(plaintext, file_name?)
let literal_wire = @packet.serialize_packet(
@packet.Literal,
literal.to_body().exact_view(),
)
let literal_wire = maybe_compress_inner(literal_wire, compress?)
let data_bc = @primitives.new_block_cipher(
@primitives.Aes256,
session_key.exact_view(),
)
let data_iv = @primitives.random_bytes(15)
let chunk_octet = 14
let header5 = Bytes::from_array([
0xD4,
1,
cipher_algo.to_byte(),
aead_algo.to_byte(),
chunk_octet.to_byte(),
])
let body = @cipher.aead_seal(
data_bc,
@cipher.Ocb,
header5.exact_view(),
data_iv.exact_view(),
chunk_octet,
literal_wire.exact_view(),
)
let aead : @packet.AeadPacket = {
version: 1,
cipher_algo,
aead_algo,
chunk_octet,
iv: data_iv,
ciphertext: body,
}
let w = @packet.Writer::new()
w.bytes(
@packet.serialize_packet(@packet.Skesk, skesk.to_body().exact_view()).exact_view(),
)
w.bytes(
@packet.serialize_packet(@packet.AeadEncData, aead.to_body().exact_view()).exact_view(),
)
@armor.Message.encode(w.to_bytes().exact_view())
}
///|
/// Walk a decrypted packet stream to the literal data, decompressing on
/// the way. Trailing signature packets (one-pass) are consumed by later
/// layers. Decompressed output draws from a cumulative budget (S03): the
/// iteration cap bounds nesting depth, the budget bounds total expansion.
fn extract_literal(inner : BytesView, max_decompressed : Int32) -> Bytes raise {
let mut stream = inner
let mut budget = max_decompressed
// At most two nesting levels: SEIPD -> compressed -> literal. A
// one-pass signature packet may precede the data.
for _ in 0..<3 {
let ps = @packet.parse_packets(stream)
if ps.is_empty() {
@error.fail("api: empty inner packet stream")
}
let tag = ps[0].tag()
if tag == @packet.Literal {
return @packet.LiteralPacket::parse(ps[0].body().exact_view()).data()
}
if tag == @packet.OnePassSig {
// Re-parse the rest after the OPS packet.
if ps.length() > 1 {
let w = @packet.Writer::new()
for p in ps[1:ps.length()] {
w.bytes(
@packet.serialize_packet(p.tag(), p.body().exact_view()).exact_view(),
)
}
stream = w.to_bytes().exact_view()
continue
}
@error.fail("api: one-pass signature with no data")
}
if tag == @packet.Compressed {
let comp = @packet.CompressedPacket::parse(ps[0].body().exact_view())
match comp.algo {
0 => stream = comp.data.exact_view()
_ => {
let cap = if budget < max_decompressed {
budget
} else {
max_decompressed
}
let out = decompress_payload(comp, cap)
budget = budget - out.length()
stream = out.exact_view()
}
}
continue
}
@error.fail(
"api: expected literal or compressed packet, got tag \{tag.number()}",
)
}
@error.fail("api: literal data not found")
}
///|
/// AEAD-open a wrapped session key with any supported mode.
fn aead_open_session_key(
bc : &@primitives.BlockCipher,
mode : @cipher.AeadMode,
nonce : BytesView,
ad : BytesView,
cipher : BytesView,
) -> Bytes raise {
match mode {
@cipher.Ocb => @cipher.ocb_decrypt(bc, nonce, ad, cipher)
@cipher.Gcm => @cipher.gcm_decrypt(bc, nonce, ad, cipher)
@cipher.Eax => @cipher.eax_decrypt(bc, nonce, ad, cipher)
}
}
///|
/// AEAD-seal a session key with any supported mode.
fn aead_seal_session_key(
bc : &@primitives.BlockCipher,
mode : @cipher.AeadMode,
nonce : BytesView,
ad : BytesView,
plain : BytesView,
) -> Bytes raise {
match mode {
@cipher.Ocb => @cipher.ocb_encrypt(bc, nonce, ad, plain)
@cipher.Gcm => @cipher.gcm_encrypt(bc, nonce, ad, plain)
@cipher.Eax => @cipher.eax_encrypt(bc, nonce, ad, plain)
}
}
///|
/// Open a v2 SEIPD packet (RFC 9580 §5.13.2): derive the message key and
/// nonce prefix from the session key and the packet salt, and decrypt the
/// chunked AEAD payload. Returns the raw inner packet stream, so callers
/// that need the signature packets get them too.
fn open_seipd_v2(
seipd : @packet.SeipdPacket,
session_key : BytesView,
) -> Bytes raise {
let algo = @error.require(
@primitives.cipher_algo_of_number(seipd.cipher_algo()),
"api: unsupported SEIPD v2 cipher",
)
let mode = @error.require(
@cipher.aead_mode_of_number(seipd.aead_algo()),
"api: unknown SEIPD v2 AEAD algorithm",
)
guard session_key.length() == algo.key_size() else {
@error.fail("api: session key length does not match the SEIPD v2 cipher")
}
let header5 = Bytes::from_array([
0xD2,
2,
seipd.cipher_algo().to_byte(),
seipd.aead_algo().to_byte(),
seipd.chunk_octet().to_byte(),
])
let (key, iv) = @cipher.seipd_v2_kdf(
session_key,
seipd.salt().exact_view(),
algo.key_size(),
mode,
header5.exact_view(),
)
let bc = @primitives.new_block_cipher(algo, key.exact_view())
let inner = @cipher.seipd_v2_open(
bc,
mode,
header5.exact_view(),
iv.exact_view(),
seipd.chunk_octet(),
seipd.ciphertext.exact_view(),
) catch {
_ =>
@error.fail(
"api: SEIPD v2 decryption failed (wrong key or corrupted message)",
)
}
inner
}
///|
/// Open a v2 SEIPD packet and extract its literal payload.
fn decrypt_seipd_v2(
seipd : @packet.SeipdPacket,
session_key : BytesView,
max_decompressed : Int32,
) -> Bytes raise {
extract_literal(open_seipd_v2(seipd, session_key), max_decompressed)
}