paysec_keyblock/tr31_2018/tr31.rs
1//! TR-31 key block wrapping and unwrapping.
2//!
3//! This module implements TR-31 key block version `D` according to
4//! ASC X9 TR 31-2018.
5//!
6//! Version `D` uses AES for key-block protection. The Key Block Protection Key
7//! (KBPK) is used to derive:
8//!
9//! - the Key Block Encryption Key (KBEK), and
10//! - the Key Block Authentication Key (KBAK).
11//!
12//! The KBEK is used for AES-CBC encryption and decryption of the confidential
13//! payload. The KBAK is used to calculate an AES-CMAC over the header and
14//! plaintext payload.
15//!
16//! Cryptographic operations are delegated to a provider implementing the
17//! required interfaces from `paysec-crypto`. This allows the TR-31
18//! implementation to remain independent from any particular AES library and
19//! permits providers to represent KBPK, KBEK, and KBAK using either software
20//! key material or opaque key handles.
21//!
22//! # Key Block Format
23//!
24//! A TR-31 key block consists of:
25//!
26//! 1. A clear-text key block header.
27//! 2. An encrypted confidential payload containing the protected key.
28//! 3. A 16-byte message authentication code.
29//!
30//! # Supported Version
31//!
32//! Only TR-31 version `D` is currently supported.
33//!
34//! # Error Handling
35//!
36//! TR-31 formatting, parsing, validation, and authentication failures are
37//! reported through [`crate::Tr31Error`].
38//!
39//! Errors returned by the selected cryptographic provider, including key
40//! derivation, AES-CMAC, and AES-CBC failures, are preserved through
41//! [`crate::Tr31CryptoError::Crypto`].
42//!
43//! # Random Padding
44//!
45//! Random data used for payload padding must be supplied by the caller. This
46//! crate does not generate randomness or assess its entropy quality.
47//!
48//! # Security
49//!
50//! The cryptographic security properties of wrapping and unwrapping depend on
51//! the selected crypto provider. Production environments should use a provider
52//! appropriate for their security requirements, such as an HSM-backed provider
53//! where required.
54//!
55//! Plaintext key material returned by unwrapping is held in [`crate::SecretKey`].
56//! The wrapper redacts debug output and zeroizes its owned bytes when dropped.
57//! Temporary plaintext payload buffers created internally during wrapping and
58//! unwrapping are also zeroized when they leave scope.
59//!
60//! These measures provide defense in depth against accidental disclosure and
61//! residual process-memory contents. They do not replace the stronger
62//! protection provided by an HSM or guarantee that callers have not retained
63//! additional copies of plaintext key material.
64//!
65//! # Example
66//!
67//! ```
68//! use paysec_crypto::AesKeySize;
69//! use paysec_crypto_soft_aes::SoftAesProvider;
70//! use paysec_keyblock::{
71//! tr31_unwrap,
72//! tr31_wrap,
73//! KeyBlockHeader,
74//! };
75//!
76//! let provider = SoftAesProvider::new();
77//!
78//! let header = KeyBlockHeader::new_with_values(
79//! "D",
80//! "P0",
81//! "A",
82//! "E",
83//! "00",
84//! "E",
85//! )
86//! .unwrap();
87//!
88//! let key =
89//! hex::decode("3F419E1CB7079442AA37474C2EFBF8B8")
90//! .unwrap();
91//!
92//! let random_seed =
93//! hex::decode("1C2965473CE206BB855B01533782")
94//! .unwrap();
95//!
96//! let kbpk = hex::decode(
97//! "88E1AB2A2E3DD38C1FA039A536500CC8A87AB9D62DC92C01058FA79F44657DE6",
98//! )
99//! .unwrap();
100//!
101//! let key_block = tr31_wrap(
102//! &provider,
103//! kbpk.as_slice(),
104//! AesKeySize::Bits256,
105//! header,
106//! &key,
107//! 0,
108//! &random_seed,
109//! )
110//! .unwrap();
111//!
112//! let expected =
113//! "D0112P0AE00E0000B82679114F470F540165EDFBF7E250FCEA43F810D215F8D207E2E417C07156A27E8E31DA05F7425509593D03A457DC34";
114//!
115//! assert_eq!(key_block, expected);
116//!
117//! let (_, unwrapped_key) = tr31_unwrap(
118//! &provider,
119//! kbpk.as_slice(),
120//! AesKeySize::Bits256,
121//! &key_block,
122//! )
123//! .unwrap();
124//!
125//! assert_eq!(unwrapped_key.expose_secret(), key.as_slice());
126//! ```
127
128use super::error::{Tr31CryptoError, Tr31Error};
129use super::key_block_header::KeyBlockHeader;
130use super::key_derivations::derive_keys_version_d;
131use super::payload::{construct_payload, extract_key_from_payload};
132
133use crate::SecretKey;
134use zeroize::Zeroizing;
135
136use paysec_crypto::{AesCbc, AesCmac, AesCmacKeyDerivation, AesKeySize};
137
138const TR31_D_MAC_LEN: usize = 16;
139const TR31_D_BLOCK_LEN: usize = 16;
140const TR31_MAX_KEY_BLOCK_LENGTH: usize = 9999;
141
142/// Wrap a cryptographic key according to TR-31 key block version `D`.
143///
144/// The KBPK is provider-specific. The provider derives KBEK and KBAK from
145/// the KBPK, calculates the authentication code using KBAK, and encrypts the
146/// confidential payload using KBEK.
147///
148/// # Parameters
149///
150/// * `provider` - Cryptographic provider used for key derivation, AES-CMAC,
151/// and AES-CBC encryption.
152/// * `kbpk` - Provider-specific Key Block Protection Key.
153/// * `kbpk_size` - AES key size of the KBPK and derived keys.
154/// * `header` - Key block header. Its key-block length field is updated by
155/// this function.
156/// * `key` - Cryptographic key or sensitive data to protect.
157/// * `masked_key_len` - Optional masked key length. A value of zero, or a
158/// value smaller than the actual key length, disables masking.
159/// * `random_seed` - Random data used for payload padding.
160///
161/// # Returns
162///
163/// The complete ASCII-encoded TR-31 key block.
164///
165/// # Errors
166///
167/// Returns [`Tr31CryptoError::Tr31`] if:
168///
169/// - the header does not specify version `D`,
170/// - payload construction fails,
171/// - the resulting key block length is invalid or exceeds the maximum
172/// representable TR-31 header length,
173/// - header processing fails.
174///
175/// Returns [`Tr31CryptoError::Crypto`] if the cryptographic provider reports
176/// a key-derivation, AES-CMAC, or AES-CBC encryption error.
177pub fn tr31_wrap<P, K: ?Sized>(
178 provider: &P,
179 kbpk: &K,
180 kbpk_size: AesKeySize,
181 mut header: KeyBlockHeader,
182 key: &[u8],
183 masked_key_len: usize,
184 random_seed: &[u8],
185) -> Result<String, Tr31CryptoError<P::Error>>
186where
187 P: AesCmacKeyDerivation<K>
188 + AesCbc<<P as AesCmacKeyDerivation<K>>::DerivedKey>
189 + AesCmac<<P as AesCmacKeyDerivation<K>>::DerivedKey>,
190{
191 if header.version_id() != "D" {
192 return Err(Tr31Error::UnsupportedVersion(header.version_id().to_string()).into());
193 }
194
195 // Derive KBEK and KBAK from the KBPK.
196 let (kbek, kbak) =
197 derive_keys_version_d(provider, kbpk, kbpk_size).map_err(Tr31CryptoError::Crypto)?;
198
199 // Construct the confidential payload.
200 let payload = Zeroizing::new(construct_payload(
201 key,
202 masked_key_len,
203 TR31_D_BLOCK_LEN,
204 random_seed,
205 )?);
206
207 // The serialized encrypted payload and MAC are represented as hexadecimal,
208 // so each binary byte consumes two ASCII characters.
209 let total_block_length = header.len() + (payload.len() * 2) + (TR31_D_MAC_LEN * 2);
210
211 if total_block_length % TR31_D_BLOCK_LEN != 0 {
212 return Err(Tr31Error::TotalBlockLengthNotMultiple {
213 block_length: TR31_D_BLOCK_LEN,
214 actual: total_block_length,
215 }
216 .into());
217 }
218
219 // Update the key block length before authenticating the header.
220 if total_block_length > TR31_MAX_KEY_BLOCK_LENGTH {
221 return Err(Tr31Error::KeyBlockLengthTooLarge {
222 maximum: TR31_MAX_KEY_BLOCK_LENGTH,
223 actual: total_block_length,
224 }
225 .into());
226 }
227
228 let encoded_block_length =
229 u16::try_from(total_block_length).map_err(|_| Tr31Error::KeyBlockLengthTooLarge {
230 maximum: TR31_MAX_KEY_BLOCK_LENGTH,
231 actual: total_block_length,
232 })?;
233
234 header.set_kb_length(encoded_block_length)?;
235
236 let header_str = header.export_str()?;
237
238 // MAC input is the clear-text header followed by the plaintext payload.
239 let mut mac_input = Zeroizing::new(header_str.as_bytes().to_vec());
240
241 mac_input.extend_from_slice(payload.as_slice());
242
243 // Authenticate with KBAK.
244 let mac = provider
245 .calculate_cmac(&kbak, mac_input.as_slice())
246 .map_err(Tr31CryptoError::Crypto)?;
247
248 // For TR-31 version D, the MAC is also used as the CBC IV.
249 let iv = mac;
250
251 // Encrypt the confidential payload with KBEK.
252 let encrypted_payload = provider
253 .encrypt_cbc(&kbek, &iv, payload.as_slice())
254 .map_err(Tr31CryptoError::Crypto)?;
255
256 let encrypted_payload_hex = hex::encode_upper(&encrypted_payload);
257
258 let mac_hex = hex::encode_upper(mac);
259
260 Ok(format!("{header_str}{encrypted_payload_hex}{mac_hex}"))
261}
262
263/// Wrap a cryptographic key according to TR-31 version `D` using a header
264/// supplied as a string.
265///
266/// This is a convenience wrapper around [`tr31_wrap`]. The supplied header is
267/// first parsed into a [`KeyBlockHeader`] and then passed to the normal
268/// wrapping operation.
269///
270/// # Parameters
271///
272/// * `provider` - Cryptographic provider.
273/// * `kbpk` - Provider-specific Key Block Protection Key.
274/// * `kbpk_size` - AES size of the KBPK.
275/// * `header_str` - String representation of the TR-31 header.
276/// * `key` - Cryptographic key or sensitive data to protect.
277/// * `masked_key_len` - Optional masked key length.
278/// * `random_seed` - Random data used for payload padding.
279///
280/// # Returns
281///
282/// The complete ASCII-encoded TR-31 key block.
283///
284/// # Errors
285///
286/// Returns [`Tr31CryptoError::Tr31`] if the header cannot be parsed or if any
287/// TR-31 wrapping, payload, or header operation fails.
288///
289/// Returns [`Tr31CryptoError::Crypto`] if the cryptographic provider reports
290/// a key-derivation, AES-CMAC, or AES-CBC encryption error.
291pub fn tr31_wrap_with_header_string<P, K: ?Sized>(
292 provider: &P,
293 kbpk: &K,
294 kbpk_size: AesKeySize,
295 header_str: &str,
296 key: &[u8],
297 masked_key_len: usize,
298 random_seed: &[u8],
299) -> Result<String, Tr31CryptoError<P::Error>>
300where
301 P: AesCmacKeyDerivation<K>
302 + AesCbc<<P as AesCmacKeyDerivation<K>>::DerivedKey>
303 + AesCmac<<P as AesCmacKeyDerivation<K>>::DerivedKey>,
304{
305 let header = KeyBlockHeader::new_from_str(header_str)?;
306
307 tr31_wrap(
308 provider,
309 kbpk,
310 kbpk_size,
311 header,
312 key,
313 masked_key_len,
314 random_seed,
315 )
316}
317
318/// Unwrap a cryptographic key from a TR-31 key block version `D`.
319///
320/// The provider derives KBEK and KBAK from the supplied KBPK. KBEK is used to
321/// decrypt the confidential payload and KBAK is used to verify the key block
322/// authentication code.
323///
324/// # Parameters
325///
326/// * `provider` - Cryptographic provider used for key derivation, AES-CMAC,
327/// and AES-CBC decryption.
328/// * `kbpk` - Provider-specific Key Block Protection Key.
329/// * `kbpk_size` - AES size of the KBPK and derived keys.
330/// * `key_block` - ASCII-encoded TR-31 key block.
331///
332/// # Returns
333///
334/// The parsed [`KeyBlockHeader`] and the unwrapped plaintext key material.
335///
336/// The key is returned as [`SecretKey`], which redacts its contents from
337/// debug output and zeroizes its owned memory when dropped. Call
338/// [`SecretKey::expose_secret`] when explicit access to the raw key bytes is
339/// required.
340///
341/// # Errors
342///
343/// Returns [`Tr31CryptoError::Tr31`] if:
344///
345/// - the key block header cannot be parsed,
346/// - the encoded key block length does not match the actual length,
347/// - the key block is shorter than the required minimum,
348/// - the key block version is unsupported,
349/// - the encrypted payload or MAC is not valid hexadecimal,
350/// - the decoded MAC does not have the required length,
351/// - MAC verification fails,
352/// - the decrypted payload is invalid.
353///
354/// Returns [`Tr31CryptoError::Crypto`] if the cryptographic provider reports
355/// a key-derivation, AES-CBC decryption, or AES-CMAC calculation error.
356pub fn tr31_unwrap<P, K: ?Sized>(
357 provider: &P,
358 kbpk: &K,
359 kbpk_size: AesKeySize,
360 key_block: &str,
361) -> Result<(KeyBlockHeader, SecretKey), Tr31CryptoError<P::Error>>
362where
363 P: AesCmacKeyDerivation<K>
364 + AesCbc<<P as AesCmacKeyDerivation<K>>::DerivedKey>
365 + AesCmac<<P as AesCmacKeyDerivation<K>>::DerivedKey>,
366{
367 let header = KeyBlockHeader::new_from_str(key_block)?;
368
369 let header_len = header.len();
370
371 let key_block_len = key_block.len();
372
373 let encoded_key_block_len = header.kb_length() as usize;
374
375 if key_block_len != encoded_key_block_len {
376 return Err(Tr31Error::KeyBlockLengthMismatch {
377 expected: encoded_key_block_len,
378 actual: key_block_len,
379 }
380 .into());
381 }
382
383 let min_key_block_len = 16 + (2 * TR31_D_BLOCK_LEN) + (2 * TR31_D_MAC_LEN);
384
385 if key_block_len < min_key_block_len {
386 return Err(Tr31Error::KeyBlockBelowMinimum {
387 minimum: min_key_block_len,
388 actual: key_block_len,
389 }
390 .into());
391 }
392
393 if header.version_id() != "D" {
394 return Err(Tr31Error::UnsupportedVersion(header.version_id().to_string()).into());
395 }
396
397 let mac_hex_len = TR31_D_MAC_LEN * 2;
398
399 let encrypted_payload_hex = &key_block[header_len..key_block_len - mac_hex_len];
400
401 let mac_hex = &key_block[key_block_len - mac_hex_len..];
402
403 // Derive KBEK and KBAK from the KBPK.
404 let (kbek, kbak) =
405 derive_keys_version_d(provider, kbpk, kbpk_size).map_err(Tr31CryptoError::Crypto)?;
406
407 let encrypted_payload = hex::decode(encrypted_payload_hex)?;
408
409 let mac = hex::decode(mac_hex)?;
410
411 let iv: [u8; TR31_D_MAC_LEN] = mac.as_slice().try_into().map_err(|_| {
412 Tr31CryptoError::Tr31(Tr31Error::InvalidMacLength {
413 expected: TR31_D_MAC_LEN,
414 actual: mac.len(),
415 })
416 })?;
417
418 // Decrypt with KBEK.
419 let decrypted_payload = Zeroizing::new(
420 provider
421 .decrypt_cbc(&kbek, &iv, &encrypted_payload)
422 .map_err(Tr31CryptoError::Crypto)?,
423 );
424
425 // MAC input is the clear-text header followed by the plaintext payload.
426 let mut mac_input = Zeroizing::new(key_block[..header_len].as_bytes().to_vec());
427
428 mac_input.extend_from_slice(decrypted_payload.as_slice());
429
430 // Authenticate with KBAK.
431 let calculated_mac = provider
432 .calculate_cmac(&kbak, mac_input.as_slice())
433 .map_err(Tr31CryptoError::Crypto)?;
434
435 if mac.as_slice() != calculated_mac.as_slice() {
436 return Err(Tr31Error::MacVerificationFailed.into());
437 }
438
439 let key = extract_key_from_payload(decrypted_payload.as_slice())?;
440
441 Ok((header, SecretKey::new(key)))
442}