Skip to content

Commit e282e9d

Browse files
committed
aead: Add AES-EAX key manager support
Implement complete AES-EAX AEAD support in the aead crate, following the patterns established by tink-cc and existing tink-rust AEAD implementations. Due to limitations in the Rust eax crate, this implementation only supports 16-byte (128-bit) IVs. tink-cc supports both 12-byte and 16-byte nonce but 16-byte is the default. Key changes: - Add eax crate from RustCrypto as dependency - Implement AesEax subtle primitive with AES-128 and AES-256 support - Implement AesEaxKeyManager with key validation and generation - Add key templates for AES-128-EAX and AES-256-EAX - Register key manager and templates in library init - Add comprehensive test coverage including wycheproof vectors For testing, I tried to match the same coverage that AES-GCM key manager has. Test coverage: - Basic encrypt/decrypt with various key and message sizes - Tag length and IV size validation - Long message tests and ciphertext modification tests - 26 wycheproof test vectors (filtered from 171 for supported params) - Key manager validation and primitive instantiation tests
1 parent fe8df8b commit e282e9d

File tree

11 files changed

+864
-0
lines changed

11 files changed

+864
-0
lines changed

Cargo.lock

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

aead/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ aes-gcm = "^0.10.3"
2020
aes-gcm-siv = "^0.11.1"
2121
chacha20poly1305 = "^0.10"
2222
ctr = "^0.9.2"
23+
eax = "^0.5.0"
2324
generic-array = "^0.14.7"
2425
tink-core = "^0.3"
2526
tink-mac = "^0.3"

aead/src/aead_key_templates.rs

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,22 @@ pub fn aes256_gcm_siv_no_prefix_key_template() -> KeyTemplate {
6161
create_aes_gcm_siv_key_template(32, OutputPrefixType::Raw)
6262
}
6363

64+
/// Return a [`KeyTemplate`] that generates an AES-EAX key with the following parameters:
65+
/// - Key size: 16 bytes
66+
/// - IV size: 16 bytes
67+
/// - Output prefix type: TINK
68+
pub fn aes128_eax_key_template() -> KeyTemplate {
69+
create_aes_eax_key_template(16, 16, OutputPrefixType::Tink)
70+
}
71+
72+
/// Return a [`KeyTemplate`] that generates an AES-EAX key with the following parameters:
73+
/// - Key size: 32 bytes
74+
/// - IV size: 16 bytes
75+
/// - Output prefix type: TINK
76+
pub fn aes256_eax_key_template() -> KeyTemplate {
77+
create_aes_eax_key_template(32, 16, OutputPrefixType::Tink)
78+
}
79+
6480
/// Return a [`KeyTemplate`] that generates an AES-CTR-HMAC-AEAD key with the following parameters:
6581
/// - AES key size: 16 bytes
6682
/// - AES CTR IV size: 16 bytes
@@ -164,6 +180,25 @@ fn create_aes_gcm_siv_key_template(
164180
}
165181
}
166182

183+
/// Return an AES-EAX key template with the given key size and IV size in bytes.
184+
fn create_aes_eax_key_template(
185+
key_size: u32,
186+
iv_size: u32,
187+
output_prefix_type: OutputPrefixType,
188+
) -> KeyTemplate {
189+
let format = tink_proto::AesEaxKeyFormat {
190+
params: Some(tink_proto::AesEaxParams { iv_size }),
191+
key_size,
192+
};
193+
let mut serialized_format = Vec::new();
194+
format.encode(&mut serialized_format).unwrap(); // safe: proto-encode
195+
KeyTemplate {
196+
type_url: crate::AES_EAX_TYPE_URL.to_string(),
197+
value: serialized_format,
198+
output_prefix_type: output_prefix_type as i32,
199+
}
200+
}
201+
167202
/// Return an AES-CTR-HMAC key template with the given parameters.
168203
fn create_aes_ctr_hmac_aead_key_template(
169204
aes_key_size: u32,

aead/src/aes_eax_key_manager.rs

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
// Copyright 2026 The Tink-Rust Authors
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
//
15+
////////////////////////////////////////////////////////////////////////////////
16+
17+
//! Key manager for AES-EAX keys.
18+
19+
use crate::subtle;
20+
use tink_core::{utils::wrap_err, TinkError};
21+
use tink_proto::prost::Message;
22+
23+
/// Maximal version of AES-EAX keys.
24+
pub const AES_EAX_KEY_VERSION: u32 = 0;
25+
/// Type URL of AES-EAX keys that Tink supports.
26+
pub const AES_EAX_TYPE_URL: &str = "type.googleapis.com/google.crypto.tink.AesEaxKey";
27+
28+
/// `AesEaxKeyManager` is an implementation of the `tink_core::registry::KeyManager` trait.
29+
/// It generates new [`AesEaxKey`](tink_proto::AesEaxKey) keys and produces new instances of
30+
/// [`subtle::AesEax`].
31+
#[derive(Default)]
32+
pub(crate) struct AesEaxKeyManager {}
33+
34+
impl tink_core::registry::KeyManager for AesEaxKeyManager {
35+
/// Create a [`subtle::AesEax`] for the given serialized [`tink_proto::AesEaxKey`].
36+
fn primitive(&self, serialized_key: &[u8]) -> Result<tink_core::Primitive, TinkError> {
37+
if serialized_key.is_empty() {
38+
return Err("AesEaxKeyManager: invalid key".into());
39+
}
40+
41+
let key = tink_proto::AesEaxKey::decode(serialized_key)
42+
.map_err(|e| wrap_err("AesEaxKeyManager: invalid key", e))?;
43+
validate_key(&key)?;
44+
45+
let params = key
46+
.params
47+
.ok_or_else(|| TinkError::new("AesEaxKeyManager: missing params"))?;
48+
49+
match subtle::AesEax::new(&key.key_value, params.iv_size as usize) {
50+
Ok(p) => Ok(tink_core::Primitive::Aead(Box::new(p))),
51+
Err(e) => Err(wrap_err("AesEaxKeyManager: cannot create new primitive", e)),
52+
}
53+
}
54+
55+
/// Create a new key according to specification the given serialized
56+
/// [`tink_proto::AesEaxKeyFormat`].
57+
fn new_key(&self, serialized_key_format: &[u8]) -> Result<Vec<u8>, TinkError> {
58+
if serialized_key_format.is_empty() {
59+
return Err("AesEaxKeyManager: invalid key format".into());
60+
}
61+
62+
let key_format = tink_proto::AesEaxKeyFormat::decode(serialized_key_format)
63+
.map_err(|e| wrap_err("AesEaxKeyManager: invalid key format", e))?;
64+
validate_key_format(&key_format)
65+
.map_err(|e| wrap_err("AesEaxKeyManager: invalid key format", e))?;
66+
67+
let key_value = tink_core::subtle::random::get_random_bytes(key_format.key_size as usize);
68+
let key = tink_proto::AesEaxKey {
69+
version: AES_EAX_KEY_VERSION,
70+
params: key_format.params,
71+
key_value,
72+
};
73+
74+
let mut sk = Vec::new();
75+
key.encode(&mut sk)
76+
.map_err(|e| wrap_err("AesEaxKeyManager: failed to encode new key", e))?;
77+
78+
Ok(sk)
79+
}
80+
81+
fn type_url(&self) -> &'static str {
82+
AES_EAX_TYPE_URL
83+
}
84+
85+
fn key_material_type(&self) -> tink_proto::key_data::KeyMaterialType {
86+
tink_proto::key_data::KeyMaterialType::Symmetric
87+
}
88+
}
89+
90+
/// Validate the given [`tink_proto::AesEaxKey`].
91+
fn validate_key(key: &tink_proto::AesEaxKey) -> Result<(), TinkError> {
92+
tink_core::keyset::validate_key_version(key.version, AES_EAX_KEY_VERSION)
93+
.map_err(|e| wrap_err("AesEaxKeyManager", e))?;
94+
95+
crate::subtle::validate_aes_key_size(key.key_value.len())
96+
.map_err(|e| wrap_err("AesEaxKeyManager", e))?;
97+
98+
let params = key
99+
.params
100+
.as_ref()
101+
.ok_or_else(|| TinkError::new("AesEaxKeyManager: missing params"))?;
102+
validate_aes_eax_params(params).map_err(|e| wrap_err("AesEaxKeyManager", e))
103+
}
104+
105+
/// Validate the given [`tink_proto::AesEaxKeyFormat`].
106+
fn validate_key_format(format: &tink_proto::AesEaxKeyFormat) -> Result<(), TinkError> {
107+
crate::subtle::validate_aes_key_size(format.key_size as usize)
108+
.map_err(|e| wrap_err("AesEaxKeyManager", e))?;
109+
110+
let params = format
111+
.params
112+
.as_ref()
113+
.ok_or_else(|| TinkError::new("AesEaxKeyManager: missing params"))?;
114+
validate_aes_eax_params(params).map_err(|e| wrap_err("AesEaxKeyManager", e))
115+
}
116+
117+
/// Validate AES-EAX parameters.
118+
fn validate_aes_eax_params(params: &tink_proto::AesEaxParams) -> Result<(), TinkError> {
119+
crate::subtle::validate_iv_size(params.iv_size as usize)
120+
}

aead/src/lib.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ mod aead_key_templates;
3030
pub use aead_key_templates::*;
3131
mod aes_ctr_hmac_aead_key_manager;
3232
pub use aes_ctr_hmac_aead_key_manager::*;
33+
mod aes_eax_key_manager;
34+
pub use aes_eax_key_manager::*;
3335
mod aes_gcm_key_manager;
3436
pub use aes_gcm_key_manager::*;
3537
mod aes_gcm_siv_key_manager;
@@ -57,6 +59,8 @@ pub fn init() {
5759
INIT.call_once(|| {
5860
register_key_manager(std::sync::Arc::new(AesCtrHmacAeadKeyManager::default()))
5961
.expect("tink_aead::init() failed"); // safe: init
62+
register_key_manager(std::sync::Arc::new(AesEaxKeyManager::default()))
63+
.expect("tink_aead::init() failed"); // safe: init
6064
register_key_manager(std::sync::Arc::new(AesGcmKeyManager::default()))
6165
.expect("tink_aead::init() failed"); // safe: init
6266
register_key_manager(std::sync::Arc::new(AesGcmSivKeyManager::default()))
@@ -68,6 +72,8 @@ pub fn init() {
6872
register_key_manager(std::sync::Arc::new(KmsEnvelopeAeadKeyManager::default()))
6973
.expect("tink_aead::init() failed"); // safe:init
7074

75+
tink_core::registry::register_template_generator("AES128_EAX", aes128_eax_key_template);
76+
tink_core::registry::register_template_generator("AES256_EAX", aes256_eax_key_template);
7177
tink_core::registry::register_template_generator("AES128_GCM", aes128_gcm_key_template);
7278
tink_core::registry::register_template_generator("AES256_GCM", aes256_gcm_key_template);
7379
tink_core::registry::register_template_generator(

aead/src/subtle/aes_eax.rs

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
// Copyright 2026 The Tink-Rust Authors
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
//
15+
////////////////////////////////////////////////////////////////////////////////
16+
17+
//! AES-EAX based implementation of the [`tink_core::Aead`] trait.
18+
19+
use aes::{Aes128, Aes256};
20+
use eax::aead::{generic_array::GenericArray, Aead, KeyInit, Payload};
21+
use generic_array::typenum;
22+
use tink_core::{utils::wrap_err, TinkError};
23+
24+
/// The only IV size that this implementation supports.
25+
/// Note: The Rust eax crate only supports nonces equal to the cipher block size (16 bytes for AES).
26+
pub const AES_EAX_IV_SIZE: usize = 16;
27+
/// The only tag size that this implementation supports.
28+
pub const AES_EAX_TAG_SIZE: usize = 16;
29+
30+
#[derive(Clone)]
31+
enum AesEaxVariant {
32+
Aes128(eax::Eax<Aes128, typenum::U16>),
33+
Aes256(eax::Eax<Aes256, typenum::U16>),
34+
}
35+
36+
/// `AesEax` is an implementation of the [`tink_core::Aead`] trait.
37+
/// Note: This implementation only supports 16-byte IVs due to limitations in the underlying eax crate.
38+
#[derive(Clone)]
39+
pub struct AesEax {
40+
cipher: AesEaxVariant,
41+
}
42+
43+
impl AesEax {
44+
/// Return an [`AesEax`] instance.
45+
/// The key argument should be the AES key, either 16 or 32 bytes to select
46+
/// AES-128 or AES-256.
47+
/// The `iv_size` must be 16 bytes (128 bits).
48+
pub fn new(key: &[u8], iv_size: usize) -> Result<AesEax, TinkError> {
49+
validate_iv_size(iv_size).map_err(|e| wrap_err("AesEax", e))?;
50+
let cipher = match key.len() {
51+
16 => AesEaxVariant::Aes128(eax::Eax::new(GenericArray::from_slice(key))),
52+
32 => AesEaxVariant::Aes256(eax::Eax::new(GenericArray::from_slice(key))),
53+
l => return Err(format!("AesEax: invalid AES key size {} (want 16, 32)", l).into()),
54+
};
55+
Ok(AesEax { cipher })
56+
}
57+
}
58+
59+
impl tink_core::Aead for AesEax {
60+
/// Encrypt `pt` with `aad` as additional authenticated data. The resulting ciphertext consists
61+
/// of two parts: (1) the IV used for encryption and (2) the actual ciphertext with tag.
62+
///
63+
/// Note: AES-EAX implementation always returns ciphertext with 128-bit tag.
64+
fn encrypt(&self, pt: &[u8], aad: &[u8]) -> Result<Vec<u8>, TinkError> {
65+
let iv = tink_core::subtle::random::get_random_bytes(AES_EAX_IV_SIZE);
66+
let nonce = GenericArray::<u8, typenum::U16>::from_slice(&iv);
67+
68+
let payload = Payload { msg: pt, aad };
69+
let ct_or = match &self.cipher {
70+
AesEaxVariant::Aes128(cipher) => cipher.encrypt(nonce, payload),
71+
AesEaxVariant::Aes256(cipher) => cipher.encrypt(nonce, payload),
72+
};
73+
74+
let ct = ct_or.map_err(|e| wrap_err("AesEax", e))?;
75+
let mut ret = Vec::with_capacity(iv.len() + ct.len());
76+
ret.extend_from_slice(&iv);
77+
ret.extend_from_slice(&ct);
78+
79+
Ok(ret)
80+
}
81+
82+
/// Decrypt `ct` with `aad` as the additional authenticated data.
83+
fn decrypt(&self, ct: &[u8], aad: &[u8]) -> Result<Vec<u8>, TinkError> {
84+
if ct.len() < AES_EAX_IV_SIZE + AES_EAX_TAG_SIZE {
85+
return Err("AesEax: ciphertext too short".into());
86+
}
87+
let nonce = GenericArray::<u8, typenum::U16>::from_slice(&ct[..AES_EAX_IV_SIZE]);
88+
89+
let payload = Payload {
90+
msg: &ct[AES_EAX_IV_SIZE..],
91+
aad,
92+
};
93+
let pt_or = match &self.cipher {
94+
AesEaxVariant::Aes128(cipher) => cipher.decrypt(nonce, payload),
95+
AesEaxVariant::Aes256(cipher) => cipher.decrypt(nonce, payload),
96+
};
97+
98+
let pt = pt_or.map_err(|e| wrap_err("AesEax", e))?;
99+
Ok(pt)
100+
}
101+
}
102+
103+
/// Validate AES-EAX IV (nonce) size.
104+
pub(crate) fn validate_iv_size(iv_size: usize) -> Result<(), TinkError> {
105+
if iv_size != AES_EAX_IV_SIZE {
106+
return Err(format!(
107+
"invalid AES-EAX IV size; only {} is supported (got {})",
108+
AES_EAX_IV_SIZE, iv_size
109+
)
110+
.into());
111+
}
112+
Ok(())
113+
}

aead/src/subtle/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ mod aead;
2020
pub use self::aead::*;
2121
mod aes_ctr;
2222
pub use self::aes_ctr::*;
23+
mod aes_eax;
24+
pub use self::aes_eax::*;
2325
mod aes_gcm;
2426
pub use self::aes_gcm::*;
2527
mod aes_gcm_siv;

0 commit comments

Comments
 (0)