Skip to content

Commit cf04ea7

Browse files
committed
age-core: Add primitives::{hpke_seal, hpke_open}
1 parent d19ff79 commit cf04ea7

File tree

5 files changed

+198
-1
lines changed

5 files changed

+198
-1
lines changed

Cargo.lock

Lines changed: 112 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,9 @@ hkdf = "0.12"
3434
hmac = "0.12"
3535
sha2 = "0.10"
3636

37+
# - HPKE from RFC 9180
38+
hpke = { version = "0.12", default-features = false, features = ["alloc"] }
39+
3740
# - scrypt from RFC 7914
3841
scrypt = { version = "0.11", default-features = false }
3942

age-core/CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ to 1.0.0 are beta releases.
88

99
## [Unreleased]
1010

11+
### Added
12+
- `age_core::primitives::{hpke_seal, hpke_open}`
13+
1114
### Changed
1215
- MSRV is now 1.70.0.
1316

age-core/Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ maintenance = { status = "experimental" }
2121
# (Breaking upgrades to these require a breaking upgrade to this crate.)
2222
chacha20poly1305.workspace = true
2323
cookie-factory.workspace = true
24+
hpke.workspace = true
2425
io_tee = "0.1.1"
2526
nom.workspace = true
2627
secrecy.workspace = true
@@ -33,6 +34,9 @@ rand.workspace = true
3334
sha2.workspace = true
3435
tempfile = { version = "3.2.0", optional = true }
3536

37+
[dev-dependencies]
38+
hpke = { workspace = true, features = ["p256"] }
39+
3640
[features]
3741
plugin = ["tempfile"]
3842
unstable = []

age-core/src/primitives.rs

Lines changed: 76 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,9 +53,68 @@ pub fn hkdf(salt: &[u8], label: &[u8], ikm: &[u8]) -> [u8; 32] {
5353
okm
5454
}
5555

56+
/// `HPKE.SealBase(pk_recip, info, aad = "", plaintext)`
57+
///
58+
/// HPKE from [RFC 9180] with:
59+
/// - KDF: HKDF-SHA256
60+
/// - AEAD: ChaCha20Poly1305
61+
/// - `aad = ""` (empty)
62+
///
63+
/// # Panics
64+
///
65+
/// Panics if the configured `Kem` produces an error. The native age recipient types that
66+
/// use HPKE are configured with parameters that ensure errors either cannot occur or are
67+
/// cryptographically negligible. If you are using this method for an age plugin, ensure
68+
/// that you choose a KEM with equivalent properties.
69+
///
70+
/// [RFC 9180]: https://tools.ietf.org/html/rfc9180
71+
pub fn hpke_seal<Kem: hpke::Kem, R: rand::RngCore + rand::CryptoRng>(
72+
pk_recip: &Kem::PublicKey,
73+
info: &[u8],
74+
plaintext: &[u8],
75+
rng: &mut R,
76+
) -> (Kem::EncappedKey, Vec<u8>) {
77+
hpke::single_shot_seal::<hpke::aead::ChaCha20Poly1305, hpke::kdf::HkdfSha256, Kem, R>(
78+
&hpke::OpModeS::Base,
79+
pk_recip,
80+
info,
81+
plaintext,
82+
&[],
83+
rng,
84+
)
85+
.expect("no errors should occur with these HPKE parameters")
86+
}
87+
88+
/// `HPKE.OpenBase(enc, sk_recip, info, aad = "", ciphertext)`
89+
///
90+
/// HPKE from [RFC 9180] with:
91+
/// - KDF: HKDF-SHA256
92+
/// - AEAD: ChaCha20Poly1305
93+
/// - `aad = ""` (empty)
94+
///
95+
/// [RFC 9180]: https://tools.ietf.org/html/rfc9180
96+
pub fn hpke_open<Kem: hpke::Kem>(
97+
encapped_key: &Kem::EncappedKey,
98+
sk_recip: &Kem::PrivateKey,
99+
info: &[u8],
100+
ciphertext: &[u8],
101+
) -> Result<Vec<u8>, hpke::HpkeError> {
102+
hpke::single_shot_open::<hpke::aead::ChaCha20Poly1305, hpke::kdf::HkdfSha256, Kem>(
103+
&hpke::OpModeR::Base,
104+
sk_recip,
105+
encapped_key,
106+
info,
107+
ciphertext,
108+
&[],
109+
)
110+
}
111+
56112
#[cfg(test)]
57113
mod tests {
58-
use super::{aead_decrypt, aead_encrypt};
114+
use hpke::Kem;
115+
use rand::rngs::OsRng;
116+
117+
use super::{aead_decrypt, aead_encrypt, hpke_open, hpke_seal};
59118

60119
#[test]
61120
fn aead_round_trip() {
@@ -65,4 +124,20 @@ mod tests {
65124
let decrypted = aead_decrypt(&key, plaintext.len(), &encrypted).unwrap();
66125
assert_eq!(decrypted, plaintext);
67126
}
127+
128+
#[test]
129+
fn hpke_round_trip() {
130+
type Kem = hpke::kem::DhP256HkdfSha256;
131+
let mut rng = OsRng;
132+
133+
let (sk_recip, pk_recip) = Kem::gen_keypair(&mut rng);
134+
135+
let info = b"foobar";
136+
let plaintext = b"12345678";
137+
138+
let (encapped_key, ciphertext) = hpke_seal::<Kem, _>(&pk_recip, info, plaintext, &mut rng);
139+
let decrypted = hpke_open::<Kem>(&encapped_key, &sk_recip, info, &ciphertext).unwrap();
140+
141+
assert_eq!(decrypted, plaintext);
142+
}
68143
}

0 commit comments

Comments
 (0)