///|
/// PKESK packet body, versions 3 (RFC 9580 §5.1.1) and 6 (§5.1.2).
pub(all) struct PkeskPacket {
version : Int32
/// v3 only: recipient key id (all zero for a hidden recipient).
key_id : Bytes
/// v6 only: recipient key version (0 for an anonymous recipient).
key_version : Int32
/// v6 only: recipient fingerprint (empty for an anonymous recipient).
fingerprint : Bytes
pub_algo : PubKeyAlgo
/// RSA: the encrypted session key MPI value. X25519/ECDH: the
/// key-wrapped session key (§5.1.5/§5.1.6), serialized after
/// `ecdh_ephemeral`.
encrypted : Bytes
/// X25519 only: the 32-octet native ephemeral public key. ECDH only:
/// the MPI value of the ephemeral point (0x40 || X). Empty for RSA.
ecdh_ephemeral : Bytes
} derive(Debug)
///|
pub fn PkeskPacket::version(self : PkeskPacket) -> Int32 {
self.version
}
///|
pub fn PkeskPacket::key_id(self : PkeskPacket) -> Bytes {
self.key_id
}
///|
pub fn PkeskPacket::key_version(self : PkeskPacket) -> Int32 {
self.key_version
}
///|
pub fn PkeskPacket::fingerprint(self : PkeskPacket) -> Bytes {
self.fingerprint
}
///|
pub fn PkeskPacket::pub_algo(self : PkeskPacket) -> PubKeyAlgo {
self.pub_algo
}
///|
pub fn PkeskPacket::encrypted(self : PkeskPacket) -> Bytes {
self.encrypted
}
///|
pub fn PkeskPacket::ecdh_ephemeral(self : PkeskPacket) -> Bytes {
self.ecdh_ephemeral
}
///|
pub fn PkeskPacket::parse(body : BytesView) -> PkeskPacket raise {
let r = Reader::new(body)
let version = r.read_u8()
guard version == 3 || version == 6 else {
@error.fail("packet: unsupported PKESK version \{version}")
}
let mut key_id = Bytes::new(0)
let mut key_version = 0
let mut fingerprint = Bytes::new(0)
if version == 6 {
// §5.1.2: a 1-octet size covering the key-version and fingerprint
// fields; zero means an anonymous recipient and both are absent. The
// fingerprint length follows from the key version (20 octets for a v4
// key, 32 for v5/v6), so any other size is malformed: a size of 1 used
// to be accepted and then re-serialized as 0, silently dropping the
// key version on an import/export round trip.
let size = r.read_u8()
if size > 0 {
key_version = r.read_u8()
let fp_len = match key_version {
4 => 20
5 | 6 => 32
_ => 0
}
guard fp_len > 0 && size == 1 + fp_len else {
@error.fail(
"packet: v6 PKESK size \{size} does not match key version \{key_version}",
)
}
fingerprint = r.read_bytes(size - 1)
}
} else {
key_id = r.read_bytes(8)
}
let algo = pub_key_algo_of_number(r.read_u8())
match algo {
X25519 => {
let eph = r.read_bytes(32)
let size = r.read_u8()
let wrapped = r.read_bytes(size)
PkeskPacket::{
version,
key_id,
key_version,
fingerprint,
pub_algo: algo,
encrypted: wrapped,
ecdh_ephemeral: eph,
}
}
Ecdh => {
let eph = r.read_mpi()
let size = r.read_u8()
let wrapped = r.read_bytes(size)
PkeskPacket::{
version,
key_id,
key_version,
fingerprint,
pub_algo: algo,
encrypted: wrapped,
ecdh_ephemeral: eph,
}
}
_ => {
let encrypted = if algo.is_rsa() { r.read_mpi() } else { r.read_rest() }
PkeskPacket::{
version,
key_id,
key_version,
fingerprint,
pub_algo: algo,
encrypted,
ecdh_ephemeral: Bytes::new(0),
}
}
}
}
///|
pub fn PkeskPacket::to_body(self : PkeskPacket) -> Bytes {
let w = Writer::new()
w.u8(self.version)
if self.version == 6 {
if self.fingerprint.length() == 0 {
w.u8(0)
} else {
w.u8(1 + self.fingerprint.length())
w.u8(self.key_version)
w.bytes(self.fingerprint.exact_view())
}
} else {
w.bytes(self.key_id.exact_view())
}
w.u8(self.pub_algo.number())
match self.pub_algo {
X25519 => {
w.bytes(self.ecdh_ephemeral.exact_view())
w.u8(self.encrypted.length())
w.bytes(self.encrypted.exact_view())
}
Ecdh => {
w.mpi(self.ecdh_ephemeral.exact_view())
w.u8(self.encrypted.length())
w.bytes(self.encrypted.exact_view())
}
_ =>
if self.pub_algo.is_rsa() {
w.mpi(self.encrypted.exact_view())
} else {
w.bytes(self.encrypted.exact_view())
}
}
w.to_bytes()
}
///|
/// SKESK packet body, versions 4 (RFC 9580 §5.3.1), 5 (the 4880bis /
/// crypto-refresh AEAD-wrapped session key GnuPG 2.4 emits under
/// --force-aead) and 6 (§5.3.2). Version 5 adds the AEAD algorithm and a
/// nonce and seals the session key with the S2K-derived key directly;
/// version 6 adds the two count octets and the HKDF-SHA256 key
/// separation (`info`/AD = tag | version | cipher | aead).
pub(all) struct SkeskPacket {
version : Int32
/// Symmetric algorithm used for the message; also the default derivation
/// length selector for the S2K key.
cipher_algo : Int32
/// AEAD algorithm id for version 5 (0 for version 4).
aead_algo : Int32
s2k : @s2k.S2k
/// AEAD IV for version 5 (empty for version 4).
iv : Bytes
/// Encrypted session key (v4: algo octet + key, CFB-wrapped; v5: the
/// AEAD seal of the key alone), present when the S2K key only wraps the
/// session key. Empty when the S2K derives it directly.
encrypted_session_key : Bytes
} derive(Debug)
///|
pub fn SkeskPacket::version(self : SkeskPacket) -> Int32 {
self.version
}
///|
pub fn SkeskPacket::cipher_algo(self : SkeskPacket) -> Int32 {
self.cipher_algo
}
///|
pub fn SkeskPacket::aead_algo(self : SkeskPacket) -> Int32 {
self.aead_algo
}
///|
pub fn SkeskPacket::s2k(self : SkeskPacket) -> @s2k.S2k {
self.s2k
}
///|
pub fn SkeskPacket::iv(self : SkeskPacket) -> Bytes {
self.iv
}
///|
pub fn SkeskPacket::encrypted_session_key(self : SkeskPacket) -> Bytes {
self.encrypted_session_key
}
///|
/// IV length of an AEAD algorithm id (RFC 9580 §9.6): 1 EAX=16, 2 OCB=15,
/// 3 GCM=12. Local table (mirroring @cipher.AeadMode::iv_length) keeps the
/// packet layer free of a cipher-package dependency, like the cipher
/// block-size table above.
fn aead_iv_len(algo : Int32) -> Int32 raise {
match algo {
1 => 16
2 => 15
3 => 12
_ => @error.fail("packet: unknown AEAD algorithm \{algo}")
}
}
///|
pub fn SkeskPacket::parse(body : BytesView) -> SkeskPacket raise {
let r = Reader::new(body)
let version = r.read_u8()
guard version == 4 || version == 5 || version == 6 else {
@error.fail("packet: unsupported SKESK version \{version}")
}
// §5.3.2: a v6 packet opens with a 1-octet count of the cipher, AEAD
// mode, S2K-length, specifier and nonce fields.
let v6_params_len = if version == 6 {
let n = r.read_u8()
guard rest_after(r, n) else {
@error.fail("packet: truncated v6 SKESK parameters")
}
n
} else {
0
}
let cipher_algo = r.read_u8()
let aead_algo = if version == 5 || version == 6 { r.read_u8() } else { 0 }
let (s2k, spec_len) = if version == 6 {
let declared = r.read_u8()
guard rest_after(r, declared) else {
@error.fail("packet: truncated v6 SKESK S2K specifier")
}
let (s2k, used) = @s2k.parse(r.view()[0:declared])
guard used == declared else {
@error.fail("packet: v6 SKESK S2K specifier length mismatch")
}
ignore(r.read_bytes(declared))
(s2k, declared)
} else {
let (s2k, used) = @s2k.parse(r.view())
ignore(r.read_bytes(used))
(s2k, used)
}
let iv = if version == 5 || version == 6 {
r.read_bytes(aead_iv_len(aead_algo))
} else {
Bytes::new(0)
}
if version == 6 {
guard v6_params_len == 3 + spec_len + iv.length() else {
@error.fail("packet: v6 SKESK parameter count mismatch")
}
}
let esk = r.read_rest()
SkeskPacket::{
version,
cipher_algo,
aead_algo,
s2k,
iv,
encrypted_session_key: esk,
}
}
///|
pub fn SkeskPacket::to_body(self : SkeskPacket) -> Bytes raise {
let w = Writer::new()
w.u8(self.version)
let spec = self.s2k.encode()
if self.version != 4 {
guard self.iv.length() == aead_iv_len(self.aead_algo) else {
@error.fail("packet: bad SKESK IV length")
}
}
if self.version == 6 {
w.u8(3 + spec.length() + self.iv.length())
}
w.u8(self.cipher_algo)
if self.version == 5 || self.version == 6 {
w.u8(self.aead_algo)
}
if self.version == 6 {
w.u8(spec.length())
}
w.bytes(spec.exact_view())
if self.version == 5 || self.version == 6 {
w.bytes(self.iv.exact_view())
}
w.bytes(self.encrypted_session_key.exact_view())
w.to_bytes()
}
///|
/// AEAD Encrypted Data Packet (tag 20) body, version 1
/// (draft-ietf-openpgp-rfc4880bis §5.16) — the shape GnuPG 2.4 produces
/// under --force-aead. RFC 9580 reserves tag 20 in favour of SEIPD v2;
/// this packet exists here for GnuPG interop and legacy data.
pub(all) struct AeadPacket {
version : Int32
cipher_algo : Int32
aead_algo : Int32
chunk_octet : Int32
iv : Bytes
/// Chunk ciphertexts+tags followed by the final tag.
ciphertext : Bytes
} derive(Debug)
///|
pub fn AeadPacket::version(self : AeadPacket) -> Int32 {
self.version
}
///|
pub fn AeadPacket::cipher_algo(self : AeadPacket) -> Int32 {
self.cipher_algo
}
///|
pub fn AeadPacket::aead_algo(self : AeadPacket) -> Int32 {
self.aead_algo
}
///|
pub fn AeadPacket::chunk_octet(self : AeadPacket) -> Int32 {
self.chunk_octet
}
///|
pub fn AeadPacket::iv(self : AeadPacket) -> Bytes {
self.iv
}
///|
pub fn AeadPacket::ciphertext(self : AeadPacket) -> Bytes {
self.ciphertext
}
///|
pub fn AeadPacket::parse(body : BytesView) -> AeadPacket raise {
let r = Reader::new(body)
let version = r.read_u8()
guard version == 1 else {
@error.fail("packet: unsupported AEAD packet version \{version}")
}
let cipher_algo = r.read_u8()
let aead_algo = r.read_u8()
let chunk_octet = r.read_u8()
let iv = r.read_bytes(aead_iv_len(aead_algo))
let ct = r.read_rest()
AeadPacket::{
version,
cipher_algo,
aead_algo,
chunk_octet,
iv,
ciphertext: ct,
}
}
///|
pub fn AeadPacket::to_body(self : AeadPacket) -> Bytes raise {
guard self.version == 1 else {
@error.fail("packet: unsupported AEAD packet version \{self.version}")
}
guard self.iv.length() == aead_iv_len(self.aead_algo) else {
@error.fail("packet: bad AEAD packet IV length")
}
let w = Writer::new()
w.u8(self.version)
w.u8(self.cipher_algo)
w.u8(self.aead_algo)
w.u8(self.chunk_octet)
w.bytes(self.iv.exact_view())
w.bytes(self.ciphertext.exact_view())
w.to_bytes()
}
///|
/// Literal data packet body (RFC 9580 §5.9).
pub struct LiteralPacket {
/// 'b' binary, 'u' UTF-8 text (or legacy 't', reported as 'u').
format : Char
file_name : String
/// Modification date, seconds since epoch.
date : Int32
data : Bytes
} derive(Debug)
///|
pub fn LiteralPacket::format(self : LiteralPacket) -> Char {
self.format
}
///|
pub fn LiteralPacket::file_name(self : LiteralPacket) -> String {
self.file_name
}
///|
pub fn LiteralPacket::date(self : LiteralPacket) -> Int32 {
self.date
}
///|
pub fn LiteralPacket::data(self : LiteralPacket) -> Bytes {
self.data
}
///|
pub fn LiteralPacket::parse(body : BytesView) -> LiteralPacket raise {
let r = Reader::new(body)
let fmt = r.read_u8()
let name_len = r.read_u8()
let name_bytes = r.read_bytes(name_len)
let date = r.read_u32()
let data = r.read_rest()
let format : Char = match fmt {
0x62 => 'b'
0x75 => 'u'
0x74 => 'u'
0x6c => 'b'
_ => 'b'
}
// Lossy on purpose (S10): the file name is attacker-controlled wire
// data; invalid UTF-8 becomes U+FFFD rather than a silent "" that
// would quietly drop the name on import/export round-trips.
let name : String = @utf8.decode_lossy(name_bytes.exact_view())
LiteralPacket::{ format, file_name: name, date, data, }
}
///|
pub fn LiteralPacket::to_body(self : LiteralPacket) -> Bytes raise {
let w = Writer::new()
w.u8(
match self.format {
'u' => 0x75
_ => 0x62
},
)
let name = @primitives.utf8_bytes(self.file_name)
guard name.length() <= 255 else {
@error.fail("packet: literal file name too long")
}
w.u8(name.length())
w.bytes(name.exact_view())
w.u32(self.date)
w.bytes(self.data.exact_view())
w.to_bytes()
}
///|
/// Build a literal packet around raw data (text uses format 'b' to avoid
/// any newline conversion unless the caller explicitly wants 'u').
pub fn LiteralPacket::binary(
data : BytesView,
file_name? : String,
) -> LiteralPacket {
LiteralPacket::{
format: 'b',
file_name: match file_name {
Some(n) => n
None => ""
},
date: 0,
data: data.to_owned(),
}
}
///|
/// Compressed data packet body (RFC 9580 §5.6): algorithm octet + data.
pub struct CompressedPacket {
/// 0 = uncompressed, 1 = ZIP (raw DEFLATE), 2 = ZLIB, 3 = BZip2.
algo : Int32
data : Bytes
} derive(Debug)
///|
pub fn CompressedPacket::parse(body : BytesView) -> CompressedPacket raise {
let r = Reader::new(body)
let algo = r.read_u8()
let data = r.read_rest()
CompressedPacket::{ algo, data, }
}
///|
pub fn CompressedPacket::to_body(self : CompressedPacket) -> Bytes {
let w = Writer::new()
w.u8(self.algo)
w.bytes(self.data.exact_view())
w.to_bytes()
}
///|
/// SEIPD packet body: version 1 (RFC 9580 §5.13.1, the MDC-protected CFB
/// shape) and version 2 (§5.13.2, the chunked AEAD shape).
pub(all) struct SeipdPacket {
version : Int32
/// v2 only: symmetric cipher algorithm id.
cipher_algo : Int32
/// v2 only: AEAD algorithm id.
aead_algo : Int32
/// v2 only: chunk-size octet (`chunk = 2^(c+6)` octets).
chunk_octet : Int32
/// v2 only: 32-octet HKDF salt.
salt : Bytes
ciphertext : Bytes
} derive(Debug)
///|
pub fn SeipdPacket::cipher_algo(self : SeipdPacket) -> Int32 {
self.cipher_algo
}
///|
pub fn SeipdPacket::aead_algo(self : SeipdPacket) -> Int32 {
self.aead_algo
}
///|
pub fn SeipdPacket::chunk_octet(self : SeipdPacket) -> Int32 {
self.chunk_octet
}
///|
pub fn SeipdPacket::salt(self : SeipdPacket) -> Bytes {
self.salt
}
///|
/// A v1 SEIPD packet literal.
pub fn SeipdPacket::v1(ciphertext : BytesView) -> SeipdPacket {
{
version: 1,
cipher_algo: 0,
aead_algo: 0,
chunk_octet: 0,
salt: Bytes::new(0),
ciphertext: ciphertext.to_owned(),
}
}
///|
pub fn SeipdPacket::parse(body : BytesView) -> SeipdPacket raise {
let r = Reader::new(body)
let version = r.read_u8()
if version == 2 {
let cipher_algo = r.read_u8()
let aead_algo = r.read_u8()
let chunk_octet = r.read_u8()
let salt = r.read_bytes(32)
let ct = r.read_rest()
SeipdPacket::{
version,
cipher_algo,
aead_algo,
chunk_octet,
salt,
ciphertext: ct,
}
} else if version == 1 {
let ct = r.read_rest()
SeipdPacket::v1(ct)
} else {
@error.fail("packet: unsupported SEIPD version \{version}")
}
}
///|
pub fn SeipdPacket::to_body(self : SeipdPacket) -> Bytes {
let w = Writer::new()
w.u8(self.version)
if self.version == 2 {
w.u8(self.cipher_algo)
w.u8(self.aead_algo)
w.u8(self.chunk_octet)
w.bytes(self.salt.exact_view())
}
w.bytes(self.ciphertext.exact_view())
w.to_bytes()
}
///|
/// Session-key checksum (RFC 9580 §5.1): sum of octets mod 65536.
pub fn session_key_checksum(key : BytesView) -> Int32 {
let mut sum = 0
for b in key {
sum = (sum + b.to_int()) & 0xFFFF
}
sum
}
///|
pub extend AeadPacket with @moonbitlang/core/debug.Debug::{to_repr}
///|
pub extend CompressedPacket with @moonbitlang/core/debug.Debug::{to_repr}
///|
pub extend LiteralPacket with @moonbitlang/core/debug.Debug::{to_repr}
///|
pub extend PkeskPacket with @moonbitlang/core/debug.Debug::{to_repr}
///|
pub extend SeipdPacket with @moonbitlang/core/debug.Debug::{to_repr}
///|
pub extend SkeskPacket with @moonbitlang/core/debug.Debug::{to_repr}