1use std::collections::BTreeMap;
2use std::fmt;
3
4use aes_gcm::aead::{Aead, KeyInit};
5use aes_gcm::{Aes256Gcm, Nonce};
6use hkdf::Hkdf;
7use p256::ecdh::diffie_hellman;
8use p256::elliptic_curve::sec1::ToEncodedPoint;
9use p256::{PublicKey, SecretKey};
10use sha2::Sha256;
11
12use super::secret_material::{base64_url_decode, base64_url_encode};
13use super::Value;
14
15const WRAPPED_SECRET_INFO: &[u8] = b"this.me/wrapped-secret/v1";
16const P256_SCALAR_LENGTH: usize = 32;
17const P256_COORD_LENGTH: usize = 32;
18const WRAPPED_SECRET_SALT_LENGTH: usize = 32;
19const WRAPPED_SECRET_IV_LENGTH: usize = 12;
20const AES_GCM_TAG_LENGTH: usize = 16;
21
22#[derive(Clone, PartialEq, Eq)]
23pub struct P256PrivateKey([u8; P256_SCALAR_LENGTH]);
24
25impl fmt::Debug for P256PrivateKey {
26 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
27 f.debug_tuple("P256PrivateKey")
28 .field(&"<redacted>")
29 .finish()
30 }
31}
32
33impl P256PrivateKey {
34 pub fn from_bytes(bytes: [u8; P256_SCALAR_LENGTH]) -> Result<Self, WrappedSecretError> {
35 SecretKey::from_slice(&bytes).map_err(|_| WrappedSecretError::InvalidPrivateKey)?;
36 Ok(Self(bytes))
37 }
38
39 pub fn from_slice(bytes: &[u8]) -> Result<Self, WrappedSecretError> {
40 let bytes: [u8; P256_SCALAR_LENGTH] = bytes
41 .try_into()
42 .map_err(|_| WrappedSecretError::InvalidPrivateKey)?;
43 Self::from_bytes(bytes)
44 }
45
46 pub fn to_bytes(&self) -> Vec<u8> {
47 self.0.to_vec()
48 }
49
50 fn as_secret_key(&self) -> Result<SecretKey, WrappedSecretError> {
51 SecretKey::from_slice(&self.0).map_err(|_| WrappedSecretError::InvalidPrivateKey)
52 }
53}
54
55#[derive(Debug, Clone, PartialEq, Eq)]
56pub struct P256PublicKeyCoordinates {
57 pub kty: String,
58 pub crv: String,
59 pub x: String,
60 pub y: String,
61}
62
63#[derive(Debug, Clone, PartialEq, Eq)]
64pub struct P256KeyPair {
65 pub private_key: P256PrivateKey,
66 pub public_key: P256PublicKeyCoordinates,
67}
68
69#[derive(Debug, Clone, Copy, PartialEq, Eq)]
70pub enum WrappedSecretOutput {
71 Bytes,
72 Utf8,
73}
74
75#[derive(Debug, Clone, PartialEq, Eq)]
76pub enum WrappedSecretCleartext {
77 Bytes(Vec<u8>),
78 Utf8(String),
79}
80
81#[derive(Debug, Clone, PartialEq, Eq)]
82pub enum WrappedSecretError {
83 RandomUnavailable,
84 InvalidPrivateKey,
85 InvalidPublicKey,
86 InvalidEnvelope,
87 UnsupportedVersion(u64),
88 UnsupportedKeyExchange,
89 UnsupportedKdf,
90 UnsupportedAead,
91 InvalidBase64Url,
92 HkdfExpandFailed,
93 EncryptFailed,
94 DecryptFailed,
95 Utf8Failed,
96}
97
98impl fmt::Display for WrappedSecretError {
99 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
100 match self {
101 Self::RandomUnavailable => write!(f, "secure random bytes are unavailable"),
102 Self::InvalidPrivateKey => write!(f, "private key is not a valid P-256 scalar"),
103 Self::InvalidPublicKey => write!(f, "public key is not a valid P-256 EC key"),
104 Self::InvalidEnvelope => write!(f, "wrapped secret envelope is invalid"),
105 Self::UnsupportedVersion(version) => {
106 write!(f, "unsupported wrapped secret version: {version}")
107 }
108 Self::UnsupportedKeyExchange => write!(f, "unsupported key exchange algorithm"),
109 Self::UnsupportedKdf => write!(f, "unsupported KDF"),
110 Self::UnsupportedAead => write!(f, "unsupported AEAD"),
111 Self::InvalidBase64Url => write!(f, "invalid base64url material in wrapped secret"),
112 Self::HkdfExpandFailed => write!(f, "HKDF expansion failed"),
113 Self::EncryptFailed => write!(f, "wrapped secret encryption failed"),
114 Self::DecryptFailed => write!(f, "wrapped secret decryption failed"),
115 Self::Utf8Failed => write!(f, "wrapped secret cleartext is not valid UTF-8"),
116 }
117 }
118}
119
120impl std::error::Error for WrappedSecretError {}
121
122pub fn generate_p256_key_pair() -> Result<P256KeyPair, WrappedSecretError> {
123 let private_key = generate_p256_private_key()?;
124 let public_key = export_p256_public_key_from_private(&private_key)?;
125 Ok(P256KeyPair {
126 private_key,
127 public_key,
128 })
129}
130
131pub fn export_p256_public_key_from_private(
132 private_key: &P256PrivateKey,
133) -> Result<P256PublicKeyCoordinates, WrappedSecretError> {
134 let secret = private_key.as_secret_key()?;
135 export_p256_public_key(&secret.public_key())
136}
137
138pub fn wrap_secret_v1(
139 secret: impl AsRef<[u8]>,
140 recipient_public_key: &P256PublicKeyCoordinates,
141 kid: &str,
142 class: &str,
143 public_key: Option<&P256PublicKeyCoordinates>,
144 policy: Option<Value>,
145) -> Result<Value, WrappedSecretError> {
146 let recipient_public_key = import_p256_public_key(recipient_public_key)?;
147 let ephemeral_private_key = generate_p256_private_key()?;
148 let ephemeral_secret = ephemeral_private_key.as_secret_key()?;
149 let shared_secret = derive_shared_secret(&ephemeral_secret, &recipient_public_key);
150
151 let salt = random_bytes::<WRAPPED_SECRET_SALT_LENGTH>()?;
152 let iv = random_bytes::<WRAPPED_SECRET_IV_LENGTH>()?;
153 let aes_key = derive_wrapping_aes_key(&shared_secret, &salt)?;
154 let cipher =
155 Aes256Gcm::new_from_slice(&aes_key).map_err(|_| WrappedSecretError::EncryptFailed)?;
156 let sealed = cipher
157 .encrypt(Nonce::from_slice(&iv), secret.as_ref())
158 .map_err(|_| WrappedSecretError::EncryptFailed)?;
159 if sealed.len() < AES_GCM_TAG_LENGTH {
160 return Err(WrappedSecretError::EncryptFailed);
161 }
162 let (ciphertext, tag) = sealed.split_at(sealed.len() - AES_GCM_TAG_LENGTH);
163 let ephemeral_public_key = export_p256_public_key(&ephemeral_secret.public_key())?;
164
165 let mut root = BTreeMap::new();
166 root.insert("version".to_string(), Value::from(1_u64));
167 root.insert("class".to_string(), Value::from(class.trim().to_string()));
168 root.insert("kid".to_string(), Value::from(kid.trim().to_string()));
169 if let Some(public_key) = public_key {
170 root.insert("publicKey".to_string(), public_key_to_value(public_key));
171 }
172 root.insert(
173 "encryption".to_string(),
174 encryption_to_value(EncryptionParts {
175 iv: &iv,
176 salt: &salt,
177 tag,
178 ciphertext,
179 ephemeral_public_key: &ephemeral_public_key,
180 }),
181 );
182 if let Some(policy) = policy {
183 root.insert("policy".to_string(), policy);
184 }
185
186 Ok(Value::Object(root))
187}
188
189pub fn unwrap_secret_v1(
190 envelope: &Value,
191 recipient_private_key: &P256PrivateKey,
192 output: WrappedSecretOutput,
193) -> Result<WrappedSecretCleartext, WrappedSecretError> {
194 let envelope = parse_envelope(envelope)?;
195 if envelope.version != 1 {
196 return Err(WrappedSecretError::UnsupportedVersion(envelope.version));
197 }
198 if envelope.kex != "ECDH-ES" {
199 return Err(WrappedSecretError::UnsupportedKeyExchange);
200 }
201 if envelope.kdf != "HKDF-SHA-256" {
202 return Err(WrappedSecretError::UnsupportedKdf);
203 }
204 if envelope.aead != "AES-256-GCM" {
205 return Err(WrappedSecretError::UnsupportedAead);
206 }
207
208 let ephemeral_public_key = import_p256_public_key(&envelope.ephemeral_public_key)?;
209 let recipient_secret = recipient_private_key.as_secret_key()?;
210 let shared_secret = derive_shared_secret(&recipient_secret, &ephemeral_public_key);
211 let aes_key = derive_wrapping_aes_key(&shared_secret, &envelope.salt)?;
212 let cipher =
213 Aes256Gcm::new_from_slice(&aes_key).map_err(|_| WrappedSecretError::DecryptFailed)?;
214 let mut payload = envelope.ciphertext;
215 payload.extend_from_slice(&envelope.tag);
216 let clear = cipher
217 .decrypt(Nonce::from_slice(&envelope.iv), payload.as_slice())
218 .map_err(|_| WrappedSecretError::DecryptFailed)?;
219
220 match output {
221 WrappedSecretOutput::Bytes => Ok(WrappedSecretCleartext::Bytes(clear)),
222 WrappedSecretOutput::Utf8 => String::from_utf8(clear)
223 .map(WrappedSecretCleartext::Utf8)
224 .map_err(|_| WrappedSecretError::Utf8Failed),
225 }
226}
227
228fn generate_p256_private_key() -> Result<P256PrivateKey, WrappedSecretError> {
229 loop {
230 let bytes = random_bytes::<P256_SCALAR_LENGTH>()?;
231 if let Ok(private_key) = P256PrivateKey::from_bytes(bytes) {
232 return Ok(private_key);
233 }
234 }
235}
236
237fn random_bytes<const N: usize>() -> Result<[u8; N], WrappedSecretError> {
238 let mut out = [0_u8; N];
239 getrandom::getrandom(&mut out).map_err(|_| WrappedSecretError::RandomUnavailable)?;
240 Ok(out)
241}
242
243fn import_p256_public_key(
244 public_key: &P256PublicKeyCoordinates,
245) -> Result<PublicKey, WrappedSecretError> {
246 if public_key.kty != "EC" || public_key.crv != "P-256" {
247 return Err(WrappedSecretError::InvalidPublicKey);
248 }
249 let x = base64_url_decode(&public_key.x).ok_or(WrappedSecretError::InvalidBase64Url)?;
250 let y = base64_url_decode(&public_key.y).ok_or(WrappedSecretError::InvalidBase64Url)?;
251 if x.len() != P256_COORD_LENGTH || y.len() != P256_COORD_LENGTH {
252 return Err(WrappedSecretError::InvalidPublicKey);
253 }
254 let mut sec1 = Vec::with_capacity(1 + P256_COORD_LENGTH * 2);
255 sec1.push(0x04);
256 sec1.extend_from_slice(&x);
257 sec1.extend_from_slice(&y);
258 PublicKey::from_sec1_bytes(&sec1).map_err(|_| WrappedSecretError::InvalidPublicKey)
259}
260
261fn export_p256_public_key(
262 public_key: &PublicKey,
263) -> Result<P256PublicKeyCoordinates, WrappedSecretError> {
264 let point = public_key.to_encoded_point(false);
265 let x = point.x().ok_or(WrappedSecretError::InvalidPublicKey)?;
266 let y = point.y().ok_or(WrappedSecretError::InvalidPublicKey)?;
267 Ok(P256PublicKeyCoordinates {
268 kty: "EC".to_string(),
269 crv: "P-256".to_string(),
270 x: base64_url_encode(x),
271 y: base64_url_encode(y),
272 })
273}
274
275fn derive_shared_secret(secret_key: &SecretKey, public_key: &PublicKey) -> [u8; 32] {
276 let shared_secret = diffie_hellman(secret_key.to_nonzero_scalar(), public_key.as_affine());
277 let mut out = [0_u8; 32];
278 out.copy_from_slice(shared_secret.raw_secret_bytes().as_slice());
279 out
280}
281
282fn derive_wrapping_aes_key(
283 shared_secret: &[u8],
284 salt: &[u8],
285) -> Result<[u8; 32], WrappedSecretError> {
286 let hkdf = Hkdf::<Sha256>::new(Some(salt), shared_secret);
287 let mut out = [0_u8; 32];
288 hkdf.expand(WRAPPED_SECRET_INFO, &mut out)
289 .map_err(|_| WrappedSecretError::HkdfExpandFailed)?;
290 Ok(out)
291}
292
293struct EncryptionParts<'a> {
294 iv: &'a [u8],
295 salt: &'a [u8],
296 tag: &'a [u8],
297 ciphertext: &'a [u8],
298 ephemeral_public_key: &'a P256PublicKeyCoordinates,
299}
300
301fn encryption_to_value(parts: EncryptionParts<'_>) -> Value {
302 let mut encryption = BTreeMap::new();
303 encryption.insert("kex".to_string(), Value::from("ECDH-ES"));
304 encryption.insert("kdf".to_string(), Value::from("HKDF-SHA-256"));
305 encryption.insert("aead".to_string(), Value::from("AES-256-GCM"));
306 encryption.insert("iv".to_string(), Value::from(base64_url_encode(parts.iv)));
307 encryption.insert(
308 "salt".to_string(),
309 Value::from(base64_url_encode(parts.salt)),
310 );
311 encryption.insert("tag".to_string(), Value::from(base64_url_encode(parts.tag)));
312 encryption.insert(
313 "ciphertext".to_string(),
314 Value::from(base64_url_encode(parts.ciphertext)),
315 );
316 encryption.insert(
317 "ephemeralPK".to_string(),
318 public_key_to_value(parts.ephemeral_public_key),
319 );
320 Value::Object(encryption)
321}
322
323fn public_key_to_value(public_key: &P256PublicKeyCoordinates) -> Value {
324 let mut object = BTreeMap::new();
325 object.insert("kty".to_string(), Value::from(public_key.kty.clone()));
326 object.insert("crv".to_string(), Value::from(public_key.crv.clone()));
327 object.insert("x".to_string(), Value::from(public_key.x.clone()));
328 object.insert("y".to_string(), Value::from(public_key.y.clone()));
329 Value::Object(object)
330}
331
332struct ParsedEnvelope {
333 version: u64,
334 kex: String,
335 kdf: String,
336 aead: String,
337 iv: Vec<u8>,
338 salt: Vec<u8>,
339 tag: Vec<u8>,
340 ciphertext: Vec<u8>,
341 ephemeral_public_key: P256PublicKeyCoordinates,
342}
343
344fn parse_envelope(envelope: &Value) -> Result<ParsedEnvelope, WrappedSecretError> {
345 let root = object_ref(envelope)?;
346 let version = number_field(root, "version")?;
347 let encryption = object_ref(field(root, "encryption")?)?;
348 let iv = base64_field(encryption, "iv")?;
349 let salt = base64_field(encryption, "salt")?;
350 let tag = base64_field(encryption, "tag")?;
351 let ciphertext = base64_field(encryption, "ciphertext")?;
352 let ephemeral_public_key = public_key_from_value(field(encryption, "ephemeralPK")?)?;
353
354 Ok(ParsedEnvelope {
355 version,
356 kex: string_field(encryption, "kex")?,
357 kdf: string_field(encryption, "kdf")?,
358 aead: string_field(encryption, "aead")?,
359 iv,
360 salt,
361 tag,
362 ciphertext,
363 ephemeral_public_key,
364 })
365}
366
367pub(crate) fn public_key_from_value(
368 value: &Value,
369) -> Result<P256PublicKeyCoordinates, WrappedSecretError> {
370 let object = object_ref(value)?;
371 Ok(P256PublicKeyCoordinates {
372 kty: string_field(object, "kty")?,
373 crv: string_field(object, "crv")?,
374 x: string_field(object, "x")?,
375 y: string_field(object, "y")?,
376 })
377}
378
379fn object_ref(value: &Value) -> Result<&BTreeMap<String, Value>, WrappedSecretError> {
380 let Value::Object(object) = value else {
381 return Err(WrappedSecretError::InvalidEnvelope);
382 };
383 Ok(object)
384}
385
386fn field<'a>(
387 object: &'a BTreeMap<String, Value>,
388 key: &str,
389) -> Result<&'a Value, WrappedSecretError> {
390 object.get(key).ok_or(WrappedSecretError::InvalidEnvelope)
391}
392
393fn string_field(object: &BTreeMap<String, Value>, key: &str) -> Result<String, WrappedSecretError> {
394 let Value::String(value) = field(object, key)? else {
395 return Err(WrappedSecretError::InvalidEnvelope);
396 };
397 Ok(value.clone())
398}
399
400fn number_field(object: &BTreeMap<String, Value>, key: &str) -> Result<u64, WrappedSecretError> {
401 let Value::Number(value) = field(object, key)? else {
402 return Err(WrappedSecretError::InvalidEnvelope);
403 };
404 if *value < 0.0 || value.fract() != 0.0 {
405 return Err(WrappedSecretError::InvalidEnvelope);
406 }
407 Ok(*value as u64)
408}
409
410fn base64_field(
411 object: &BTreeMap<String, Value>,
412 key: &str,
413) -> Result<Vec<u8>, WrappedSecretError> {
414 let value = string_field(object, key)?;
415 base64_url_decode(&value).ok_or(WrappedSecretError::InvalidBase64Url)
416}