feat: replace deprecated ExpandedKeyEncoding with seed serialization, derive ZeroizeOnDrop #7

Merged
breakingbread merged 3 commits from feat/zeroize-derive-and-seed-serialization into master 2026-07-03 13:06:31 +02:00
10 changed files with 588 additions and 631 deletions
+6 -1
View File
@@ -5,7 +5,12 @@
* Fixed doc showing incorrect MSRV * Fixed doc showing incorrect MSRV
* Fixed readme incorrect doc link * Fixed readme incorrect doc link
* Bump `ecdsa` to `0.17` * Bump `ecdsa` to `0.17`
* Bump `voprf-vx` to `1.0.0-rc` * Bump `voprf-vx` to `1.0.0-rc.1`
* Added `ZeroizeOnDrop` bound to `Hash` trait
* Replaced manual `Drop` impls on `Ke2Builder` and `KemKe2Builder` with `#[derive_where(ZeroizeOnDrop)]`
* Replaced deprecated `ExpandedKeyEncoding` with seed-based serialization (`KeyInit` / `KeyExport`) for KEM decapsulation keys
* Fixed deserialization of `voprf` types to pass exact-length slices (compatibility with `voprf` trailing bytes rejection)
* Regenerated test vectors
## 1.0.0-pre.1 (July 2, 2026) ## 1.0.0-pre.1 (July 2, 2026)
+7 -7
View File
@@ -38,11 +38,11 @@ serde = [
std = ["dep:getrandom", "rand/std"] std = ["dep:getrandom", "rand/std"]
[dependencies] [dependencies]
argon2 = { version = "0.6.0-rc", default-features = false, features = [ argon2 = { version = "0.6.0-rc.8", default-features = false, features = [
"zeroize", "zeroize",
"alloc", "alloc",
], optional = true } ], optional = true }
curve25519-dalek = { version = "5.0.0-rc", default-features = false, features = [ curve25519-dalek = { version = "5.0.0-rc.1", default-features = false, features = [
"zeroize", "zeroize",
], optional = true } ], optional = true }
derive-where = { version = "1.6", features = ["zeroize-on-drop"] } derive-where = { version = "1.6", features = ["zeroize-on-drop"] }
@@ -51,7 +51,7 @@ displaydoc = { version = "0.2", default-features = false }
ecdsa = { version = "0.17", default-features = false, features = [ ecdsa = { version = "0.17", default-features = false, features = [
"algorithm", "algorithm",
], optional = true } ], optional = true }
ed25519-dalek = { version = "3.0.0-rc", default-features = false, features = [ ed25519-dalek = { version = "3.0.0-rc.1", default-features = false, features = [
"zeroize", "zeroize",
"digest", "digest",
"hazmat", "hazmat",
@@ -69,7 +69,7 @@ serde = { version = "1", default-features = false, features = [
"derive", "derive",
], optional = true } ], optional = true }
subtle = { version = "2.6", default-features = false } subtle = { version = "2.6", default-features = false }
voprf = { package = "voprf-vx", version = "1.0.0-rc", default-features = false, features = [ voprf = { package = "voprf-vx", version = "1.0.0-rc.1", default-features = false, features = [
"danger", "danger",
] } ] }
zeroize = { version = "1.9", features = ["zeroize_derive"] } zeroize = { version = "1.9", features = ["zeroize_derive"] }
@@ -85,18 +85,18 @@ criterion = "0.8"
cryptoki = "0.12" cryptoki = "0.12"
elliptic-curve = { version = "0.14", features = ["alloc", "pkcs8"] } elliptic-curve = { version = "0.14", features = ["alloc", "pkcs8"] }
hex = "0.4" hex = "0.4"
p256 = { version = "0.14.0-rc", default-features = false, features = [ p256 = { version = "0.14.0-rc.15", default-features = false, features = [
"ecdsa", "ecdsa",
"hash2curve", "hash2curve",
"pkcs8", "pkcs8",
"oprf", "oprf",
] } ] }
p384 = { version = "0.14.0-rc", default-features = false, features = [ p384 = { version = "0.14.0-rc.15", default-features = false, features = [
"hash2curve", "hash2curve",
"pkcs8", "pkcs8",
"oprf", "oprf",
] } ] }
p521 = { version = "0.14.0-rc", default-features = false, features = [ p521 = { version = "0.14.0-rc.15", default-features = false, features = [
"hash2curve", "hash2curve",
"pkcs8", "pkcs8",
"oprf", "oprf",
+1 -1
View File
@@ -30,7 +30,7 @@ Installation
Add the following line to the dependencies of your `Cargo.toml`: Add the following line to the dependencies of your `Cargo.toml`:
``` ```
opaque-ke = { package = "opaque-vx", version = "1.0.0-rc.0" } opaque-vx = "1.0.0-rc.0"
``` ```
### Minimum Supported Rust Version ### Minimum Supported Rust Version
+3 -1
View File
@@ -48,6 +48,7 @@ pub trait Hash:
+ FixedOutputReset + FixedOutputReset
+ CoreProxy + CoreProxy
+ Clone + Clone
+ zeroize::ZeroizeOnDrop
where where
<Self as CoreProxy>::Core: ProxyHash, <Self as CoreProxy>::Core: ProxyHash,
<<Self as CoreProxy>::Core as SmallBlockSizeUser>::_BlockSize: IsLess<U256>, <<Self as CoreProxy>::Core as SmallBlockSizeUser>::_BlockSize: IsLess<U256>,
@@ -64,7 +65,8 @@ impl<
+ BlockSizeUser + BlockSizeUser
+ FixedOutputReset + FixedOutputReset
+ CoreProxy + CoreProxy
+ Clone, + Clone
+ zeroize::ZeroizeOnDrop,
> Hash for T > Hash for T
where where
<T as CoreProxy>::Core: ProxyHash, <T as CoreProxy>::Core: ProxyHash,
+3 -36
View File
@@ -14,7 +14,6 @@ use generic_array::typenum::{IsLess, Le, NonZero, Sum, U256};
use generic_array::{ArrayLength, GenericArray}; use generic_array::{ArrayLength, GenericArray};
use rand::{CryptoRng, Rng}; use rand::{CryptoRng, Rng};
use subtle::{ConstantTimeEq, CtOption}; use subtle::{ConstantTimeEq, CtOption};
use zeroize::{Zeroize, ZeroizeOnDrop};
use super::{ use super::{
Deserialize, GenerateKe1Result, GenerateKe2Result, GenerateKe3Result, KeyExchange, Serialize, Deserialize, GenerateKe1Result, GenerateKe2Result, GenerateKe3Result, KeyExchange, Serialize,
@@ -69,7 +68,7 @@ pub struct Ke2State<H: OutputSizeUser> {
serialize = "H: serde::Serialize, PublicKey<G>: serde::Serialize", serialize = "H: serde::Serialize, PublicKey<G>: serde::Serialize",
)) ))
)] )]
#[derive_where(Clone)] #[derive_where(Clone, ZeroizeOnDrop)]
#[derive_where(Debug, Eq, Hash, PartialEq; H, PublicKey<G>)] #[derive_where(Debug, Eq, Hash, PartialEq; H, PublicKey<G>)]
pub struct Ke2Builder<G: Group, H: Hash> pub struct Ke2Builder<G: Group, H: Hash>
where where
@@ -80,7 +79,9 @@ where
{ {
server_nonce: GenericArray<u8, NonceLen>, server_nonce: GenericArray<u8, NonceLen>,
transcript_hasher: H, transcript_hasher: H,
#[derive_where(skip(Zeroize))]
client_e_pk: PublicKey<G>, client_e_pk: PublicKey<G>,
#[derive_where(skip(Zeroize))]
server_e_pk: PublicKey<G>, server_e_pk: PublicKey<G>,
shared_secret_1: GenericArray<u8, G::PkLen>, shared_secret_1: GenericArray<u8, G::PkLen>,
shared_secret_3: GenericArray<u8, G::PkLen>, shared_secret_3: GenericArray<u8, G::PkLen>,
@@ -352,40 +353,6 @@ where
} }
} }
/// TODO: implement via derive after `Hash` gets `Zeroize` support.
impl<G: Group, H: Hash> Drop for Ke2Builder<G, H>
where
H::Core: ProxyHash,
<<H as CoreProxy>::Core as SmallBlockSizeUser>::_BlockSize: IsLess<U256>,
Le<<<H as CoreProxy>::Core as SmallBlockSizeUser>::_BlockSize, U256>: NonZero,
OutputSize<H>: ArrayLength,
{
fn drop(&mut self) {
let Self {
server_nonce,
transcript_hasher,
client_e_pk: _,
server_e_pk: _,
shared_secret_1,
shared_secret_3,
} = self;
server_nonce.zeroize();
digest::Reset::reset(transcript_hasher);
shared_secret_1.zeroize();
shared_secret_3.zeroize();
}
}
impl<G: Group, H: Hash> ZeroizeOnDrop for Ke2Builder<G, H>
where
H::Core: ProxyHash,
<<H as CoreProxy>::Core as SmallBlockSizeUser>::_BlockSize: IsLess<U256>,
Le<<<H as CoreProxy>::Core as SmallBlockSizeUser>::_BlockSize, U256>: NonZero,
OutputSize<H>: ArrayLength,
{
}
impl<G: Group, H: Hash> Deserialize for Ke2Message<G, H> impl<G: Group, H: Hash> Deserialize for Ke2Message<G, H>
where where
H::Core: ProxyHash, H::Core: ProxyHash,
+12 -49
View File
@@ -24,15 +24,12 @@ use digest::block_api::{CoreProxy, SmallBlockSizeUser};
use generic_array::typenum::{Cmp, IsLess, Le, NonZero, Sum, U256}; use generic_array::typenum::{Cmp, IsLess, Le, NonZero, Sum, U256};
use generic_array::{ArrayLength, GenericArray}; use generic_array::{ArrayLength, GenericArray};
use hybrid_array::ArraySize; use hybrid_array::ArraySize;
#[allow(deprecated)]
use ml_kem::ExpandedKeyEncoding;
use ml_kem::kem::{ use ml_kem::kem::{
Ciphertext as MlKemCiphertext, Decapsulate, Encapsulate, Kem as MlKemTrait, KeyExport, Ciphertext as MlKemCiphertext, Decapsulate, Encapsulate, Kem as MlKemTrait, KeyExport, KeyInit,
KeySizeUser, TryKeyInit, KeySizeUser, TryKeyInit,
}; };
use rand::{CryptoRng, Rng}; use rand::{CryptoRng, Rng};
use subtle::{ConstantTimeEq, CtOption}; use subtle::{ConstantTimeEq, CtOption};
use zeroize::{Zeroize, ZeroizeOnDrop};
use super::shared::{self, Ke1Message, Ke1State, NonceLen}; use super::shared::{self, Ke1Message, Ke1State, NonceLen};
use super::{ use super::{
@@ -55,7 +52,7 @@ pub trait KemCoreWrapper {
type EncapsulationKey: Clone; type EncapsulationKey: Clone;
/// Secret key type used for decapsulation operations. /// Secret key type used for decapsulation operations.
type DecapsulationKey: Clone + ZeroizeOnDrop; type DecapsulationKey: Clone + zeroize::ZeroizeOnDrop;
/// Length (in bytes) of the serialized public key. /// Length (in bytes) of the serialized public key.
type EncapsulationKeyLen: ArrayLength + ArraySize; type EncapsulationKeyLen: ArrayLength + ArraySize;
@@ -136,18 +133,16 @@ impl<R: Rng> rand::rand_core::TryRng for RngCompat<'_, R> {
impl<R: Rng + CryptoRng> rand::rand_core::TryCryptoRng for RngCompat<'_, R> {} impl<R: Rng + CryptoRng> rand::rand_core::TryCryptoRng for RngCompat<'_, R> {}
type RcEncapsulationKeyLen<K> = <<K as MlKemTrait>::EncapsulationKey as KeySizeUser>::KeySize; type RcEncapsulationKeyLen<K> = <<K as MlKemTrait>::EncapsulationKey as KeySizeUser>::KeySize;
#[allow(deprecated)] type RcDecapsulationKeyLen<K> = <<K as MlKemTrait>::DecapsulationKey as KeySizeUser>::KeySize;
type RcDecapsulationKeyLen<K> =
<<K as MlKemTrait>::DecapsulationKey as ExpandedKeyEncoding>::EncodedSize;
type RcCiphertextLen<K> = <K as MlKemTrait>::CiphertextSize; type RcCiphertextLen<K> = <K as MlKemTrait>::CiphertextSize;
type RcSharedSecretLen<K> = <K as MlKemTrait>::SharedKeySize; type RcSharedSecretLen<K> = <K as MlKemTrait>::SharedKeySize;
#[allow(deprecated)]
impl<K> KemCoreWrapper for K impl<K> KemCoreWrapper for K
where where
K: MlKemTrait, K: MlKemTrait,
K::EncapsulationKey: Encapsulate<Kem = K> + KeyExport + TryKeyInit + Clone, K::EncapsulationKey: Encapsulate<Kem = K> + KeyExport + TryKeyInit + Clone,
K::DecapsulationKey: Decapsulate<Kem = K> + ExpandedKeyEncoding + Clone + ZeroizeOnDrop, K::DecapsulationKey:
Decapsulate<Kem = K> + KeyExport + KeyInit + Clone + zeroize::ZeroizeOnDrop,
RcEncapsulationKeyLen<K>: ArrayLength + ArraySize, RcEncapsulationKeyLen<K>: ArrayLength + ArraySize,
RcDecapsulationKeyLen<K>: ArrayLength + ArraySize, RcDecapsulationKeyLen<K>: ArrayLength + ArraySize,
RcCiphertextLen<K>: ArrayLength + ArraySize, RcCiphertextLen<K>: ArrayLength + ArraySize,
@@ -185,7 +180,7 @@ where
fn serialize_decapsulation_key( fn serialize_decapsulation_key(
key: &Self::DecapsulationKey, key: &Self::DecapsulationKey,
) -> GenericArray<u8, Self::DecapsulationKeyLen> { ) -> GenericArray<u8, Self::DecapsulationKeyLen> {
GenericArray::from_slice(key.to_expanded_bytes().as_slice()).clone() GenericArray::from_slice(key.to_bytes().as_slice()).clone()
} }
fn deserialize_decapsulation_key( fn deserialize_decapsulation_key(
@@ -193,10 +188,9 @@ where
) -> Result<Self::DecapsulationKey, ProtocolError> { ) -> Result<Self::DecapsulationKey, ProtocolError> {
let bytes: GenericArray<u8, RcDecapsulationKeyLen<K>> = let bytes: GenericArray<u8, RcDecapsulationKeyLen<K>> =
input.take_array("kem decapsulation key")?; input.take_array("kem decapsulation key")?;
let key = ml_kem::array::Array::try_from(bytes.as_slice()) let seed = ml_kem::array::Array::try_from(bytes.as_slice())
.map_err(|_| ProtocolError::SerializationError)?; .map_err(|_| ProtocolError::SerializationError)?;
K::DecapsulationKey::from_expanded_bytes(&key) Ok(KeyInit::new(&seed))
.map_err(|_| ProtocolError::SerializationError)
} }
fn encapsulate<R: Rng + CryptoRng>( fn encapsulate<R: Rng + CryptoRng>(
@@ -289,7 +283,7 @@ where
/// Server builder placeholder capturing the data needed to finish the KEM /// Server builder placeholder capturing the data needed to finish the KEM
/// exchange. /// exchange.
#[derive_where(Clone)] #[derive_where(Clone, ZeroizeOnDrop)]
pub struct KemKe2Builder<G: Group, H: Hash, K: KemCoreWrapper> pub struct KemKe2Builder<G: Group, H: Hash, K: KemCoreWrapper>
where where
H::Core: ProxyHash, H::Core: ProxyHash,
@@ -300,10 +294,13 @@ where
{ {
server_nonce: GenericArray<u8, NonceLen>, server_nonce: GenericArray<u8, NonceLen>,
transcript_hasher: H, transcript_hasher: H,
#[derive_where(skip(Zeroize))]
client_e_pk: PublicKey<G>, client_e_pk: PublicKey<G>,
#[derive_where(skip(Zeroize))]
server_e_pk: PublicKey<G>, server_e_pk: PublicKey<G>,
shared_secret_1: GenericArray<u8, G::PkLen>, shared_secret_1: GenericArray<u8, G::PkLen>,
shared_secret_3: GenericArray<u8, G::PkLen>, shared_secret_3: GenericArray<u8, G::PkLen>,
#[derive_where(skip(Zeroize))]
kem_encapsulation_key: GenericArray<u8, K::EncapsulationKeyLen>, kem_encapsulation_key: GenericArray<u8, K::EncapsulationKeyLen>,
kem_ciphertext: GenericArray<u8, K::CiphertextLen>, kem_ciphertext: GenericArray<u8, K::CiphertextLen>,
kem_shared_secret: GenericArray<u8, K::SharedSecretLen>, kem_shared_secret: GenericArray<u8, K::SharedSecretLen>,
@@ -335,40 +332,6 @@ where
/// Third message remains the same as `TripleDH`. /// Third message remains the same as `TripleDH`.
pub type KemKe3Message<H> = super::tripledh::Ke3Message<H>; pub type KemKe3Message<H> = super::tripledh::Ke3Message<H>;
impl<G, H, K> Drop for KemKe2Builder<G, H, K>
where
G: Group,
H: Hash,
H::Core: ProxyHash,
<<H as CoreProxy>::Core as SmallBlockSizeUser>::_BlockSize: IsLess<U256>,
Le<<<H as CoreProxy>::Core as SmallBlockSizeUser>::_BlockSize, U256>: NonZero,
<<H as CoreProxy>::Core as SmallBlockSizeUser>::_BlockSize: Cmp<U256>,
OutputSize<H>: ArrayLength,
K: KemCoreWrapper,
{
fn drop(&mut self) {
self.server_nonce.zeroize();
digest::Digest::reset(&mut self.transcript_hasher);
self.shared_secret_1.zeroize();
self.shared_secret_3.zeroize();
self.kem_shared_secret.zeroize();
self.kem_ciphertext.zeroize();
}
}
impl<G, H, K> ZeroizeOnDrop for KemKe2Builder<G, H, K>
where
G: Group,
H: Hash,
H::Core: ProxyHash,
<<H as CoreProxy>::Core as SmallBlockSizeUser>::_BlockSize: IsLess<U256>,
Le<<<H as CoreProxy>::Core as SmallBlockSizeUser>::_BlockSize, U256>: NonZero,
<<H as CoreProxy>::Core as SmallBlockSizeUser>::_BlockSize: Cmp<U256>,
OutputSize<H>: ArrayLength,
K: KemCoreWrapper,
{
}
impl<G, H, K> KeyExchange for TripleDhKem<G, H, K> impl<G, H, K> KeyExchange for TripleDhKem<G, H, K>
where where
G: Group + 'static, G: Group + 'static,
+22 -6
View File
@@ -233,6 +233,10 @@ impl<CS: CipherSuite> RegistrationRequest<CS> {
/// Deserialization from bytes /// Deserialization from bytes
pub fn deserialize(input: &[u8]) -> Result<Self, ProtocolError> { pub fn deserialize(input: &[u8]) -> Result<Self, ProtocolError> {
let elem_len = <OprfGroup<CS> as voprf::Group>::ElemLen::USIZE;
if input.len() < elem_len {
return Err(ProtocolError::SerializationError);
}
Ok(Self { Ok(Self {
blinded_element: BlindedElement::deserialize(input)?, blinded_element: BlindedElement::deserialize(input)?,
}) })
@@ -260,8 +264,12 @@ impl<CS: CipherSuite> RegistrationResponse<CS> {
/// Deserialization from bytes /// Deserialization from bytes
pub fn deserialize(mut input: &[u8]) -> Result<Self, ProtocolError> { pub fn deserialize(mut input: &[u8]) -> Result<Self, ProtocolError> {
let evaluation_element = EvaluationElement::deserialize(input)?; let elem_len = EvaluationElementLen::<CS::OprfCs>::USIZE;
input = &input[EvaluationElementLen::<CS::OprfCs>::USIZE..]; if input.len() < elem_len {
return Err(ProtocolError::SerializationError);
}
let evaluation_element = EvaluationElement::deserialize(&input[..elem_len])?;
input = &input[elem_len..];
Ok(Self { Ok(Self {
evaluation_element, evaluation_element,
@@ -360,8 +368,12 @@ impl<CS: CipherSuite> CredentialRequest<CS> {
where where
<CS::KeyExchange as KeyExchange>::KE1Message: Deserialize, <CS::KeyExchange as KeyExchange>::KE1Message: Deserialize,
{ {
let blinded_element = BlindedElement::deserialize(input)?; let elem_len = BlindedElementLen::<CS::OprfCs>::USIZE;
*input = &input[BlindedElementLen::<CS::OprfCs>::USIZE..]; if input.len() < elem_len {
return Err(ProtocolError::SerializationError);
}
let blinded_element = BlindedElement::deserialize(&input[..elem_len])?;
*input = &input[elem_len..];
Ok(Self { Ok(Self {
blinded_element, blinded_element,
@@ -415,8 +427,12 @@ impl<CS: CipherSuite> CredentialResponse<CS> {
where where
<CS::KeyExchange as KeyExchange>::KE2Message: Deserialize, <CS::KeyExchange as KeyExchange>::KE2Message: Deserialize,
{ {
let evaluation_element = EvaluationElement::deserialize(input)?; let elem_len = EvaluationElementLen::<CS::OprfCs>::USIZE;
input = &input[EvaluationElementLen::<CS::OprfCs>::USIZE..]; if input.len() < elem_len {
return Err(ProtocolError::SerializationError);
}
let evaluation_element = EvaluationElement::deserialize(&input[..elem_len])?;
input = &input[elem_len..];
Ok(Self { Ok(Self {
evaluation_element, evaluation_element,
+12 -4
View File
@@ -289,8 +289,12 @@ impl<CS: CipherSuite> ClientRegistration<CS> {
/// Deserialization from bytes /// Deserialization from bytes
pub fn deserialize(mut input: &[u8]) -> Result<Self, ProtocolError> { pub fn deserialize(mut input: &[u8]) -> Result<Self, ProtocolError> {
let oprf_client = OprfClient::deserialize(input)?; let client_len = OprfClientLen::<CS::OprfCs>::USIZE;
input = &input[OprfClientLen::<CS::OprfCs>::USIZE..]; if input.len() < client_len {
return Err(ProtocolError::SerializationError);
}
let oprf_client = OprfClient::deserialize(&input[..client_len])?;
input = &input[client_len..];
let blinded_element = BlindedElement::deserialize(input)?; let blinded_element = BlindedElement::deserialize(input)?;
@@ -486,8 +490,12 @@ impl<CS: CipherSuite> ClientLogin<CS> {
<CS::KeyExchange as KeyExchange>::KE1Message: Deserialize + Serialize, <CS::KeyExchange as KeyExchange>::KE1Message: Deserialize + Serialize,
<CS::KeyExchange as KeyExchange>::KE1State: Deserialize + Serialize, <CS::KeyExchange as KeyExchange>::KE1State: Deserialize + Serialize,
{ {
let oprf_client = OprfClient::deserialize(input)?; let client_len = OprfClientLen::<CS::OprfCs>::USIZE;
input = &input[OprfClientLen::<CS::OprfCs>::USIZE..]; if input.len() < client_len {
return Err(ProtocolError::SerializationError);
}
let oprf_client = OprfClient::deserialize(&input[..client_len])?;
input = &input[client_len..];
Ok(Self { Ok(Self {
oprf_client, oprf_client,
+3 -7
View File
@@ -738,16 +738,12 @@ fn generate_test_vectors() -> Result<(), ProtocolError> {
#[rustfmt::skip] #[rustfmt::skip]
output.push_str( output.push_str(
"\ "\
// SPDX-License-Identifier: MIT OR Apache-2.0\n\
// Copyright (c) VexaHub and contributors.\n\
// Copyright (c) Meta Platforms, Inc. and affiliates.\n\ // Copyright (c) Meta Platforms, Inc. and affiliates.\n\
//\n\ //\n\
// This source code is dual-licensed under either the MIT license found in the\n\
// LICENSE-MIT file in the root directory of this source tree or the Apache\n\
// License, Version 2.0 found in the LICENSE-APACHE file in the root directory\n\
// of this source tree. You may select, at your option, one of the above-listed\n\
// licenses.\n\
//\n\
// To regenerate these test vectors, run:\n\ // To regenerate these test vectors, run:\n\
// FULL_TEST_VECTORS_FILE=src/tests/full_test_vectors.rs cargo test --features curve25519,ecdsa,ed25519 -- generate_test_vectors\n\ // FULL_TEST_VECTORS_FILE=src/tests/full_test_vectors.rs cargo test --features curve25519,ecdsa,ed25519,kem -- generate_test_vectors\n\
\n\ \n\
#![allow(clippy::duplicated_attributes)]\n\ #![allow(clippy::duplicated_attributes)]\n\
\n", \n",
File diff suppressed because one or more lines are too long