This crate implements the cryptographic primitives (modern encryption and signature schemes) used in many other Cosmian products, such as the Cloudproof libraries and the KMS.
It is primarily a thin layer over the RustCrypto libraries, exposing as far as possible more straightforward and more consistent Traits, for example the lib:
GenericArray
types and uses [u8; N]
arrays only,libsodium
compatibility,This crate may disappear in the future, as the RustCrypto libraries evolve.
To use Cosmian CryptoCore, add the dependency using Cargo:
cargo add cosmian_crypto_core
This document provides examples of the most common use cases. All the examples are available in the examples directory
To install and build Cosmian CryptoCore, clone the repo:
git clone https://github.com/Cosmian/crypto_core.git
Build it with Cargo:
cargo build --release
Running the nightly backend with the following flags (SIMD instructions) will improve performance by about 33% on the Ristretto backend, 25% for AES GCM, and 15% on the ED25519 backend
RUSTFLAGS='--cfg curve25519_dalek_backend="simd" -C target_cpu=native'
Run tests using:
cargo test --release
Run benchmarks using:
cargo bench
This crate offers standardized authenticated encryption schemes: AES-GCM - with 128 and 256 keys -, Chacha20 Poly1305, and XChacha Poly1305. All these primitives are available in three modes: vector-combined, vector-detached, and streaming.
In combined and detached modes, AES 256 GCM, Chacha20-Poly1305, and XChachaPoly1305 are compatible with their IETF equivalent in libsodium
. Check this documentation for details of the libsodium
implementations.
The AES version uses AES-NI instructions when available, which is the case on most modern processors. All these primitives are fast and encrypt a vector of bytes with authentication in an average of 2.5µs on a 2.6GHz Intel Core i7.
In combined mode, the primitive encrypts a plaintext as a single vector of bytes and allocates a new vector for the ciphertext that holds the encrypted data and the MAC.
use cosmian_crypto_core::XChaCha20Poly1305;
use cosmian_crypto_core::{
reexport::rand_core::SeedableRng, CsRng, Dem, FixedSizeCBytes, Instantiable, Nonce,
RandomFixedSizeCBytes, SymmetricKey,
};
// Choose one of these DEMs depending on your use case
// type SC = Aes128Gcm;
// type SC = Aes256Gcm;
// type SC = ChaCha20Poly1305;
type SC = XChaCha20Poly1305;
// A cryptographically secure random generator
let mut rng = CsRng::from_entropy();
// the message to encrypt
let message = b"my secret message";
// the secret key used to encrypt the message
// which is shared between the sender and the recipient
let secret_key = SymmetricKey::new(&mut rng);
// the additional data shared between the sender and the recipient to authenticate the message
let additional_data = Some(b"additional data".as_slice());
// the sender generate a Nonce and encrypts the message
let nonce = Nonce::new(&mut rng);
let dem = SC::new(&secret_key);
let ciphertext = dem.encrypt(&nonce, message, additional_data).unwrap();
// to transmit the message, the sender can concatenate the nonce and the ciphertext
// and send the concatenated result to the recipient
let ciphertext = [nonce.as_bytes(), ciphertext.as_slice()].concat();
// the ciphertext size is the message size plus the nonce size plus the authentication tag size
assert_eq!(
ciphertext.len(),
message.len() + SC::NONCE_LENGTH + SC::MAC_LENGTH
);
// the recipient extracts the nonce and decrypts the message
let nonce = Nonce::try_from_slice(&ciphertext[..SC::NONCE_LENGTH]).unwrap();
let dem = SC::new(&secret_key);
let plaintext = dem
.decrypt(&nonce, &ciphertext[SC::NONCE_LENGTH..], additional_data)
.unwrap();
// assert the decrypted message is identical to the original plaintext
assert_eq!(plaintext, message, "Decryption failed");
In combined mode, the primitive encrypts a plaintext as a single mutable vector of bytes in place; it returns the MAC tag on the encryption call.
use cosmian_crypto_core::DemInPlace;
use cosmian_crypto_core::{
reexport::rand_core::SeedableRng, CsRng, FixedSizeCBytes, Instantiable, Nonce,
RandomFixedSizeCBytes, SymmetricKey, XChaCha20Poly1305,
};
// Choose one of these DEMs depending on your use case
// type SC = Aes128Gcm;
// type SC = Aes256Gcm;
// type SC = ChaCha20Poly1305;
type SC = XChaCha20Poly1305;
// A cryptographically secure random generator
let mut rng = CsRng::from_entropy();
// the message to encrypt
let message = b"my secret message";
// the secret key used to encrypt the message
// which is shared between the sender and the recipient
let secret_key = SymmetricKey::new(&mut rng);
// the additional data shared between the sender and the recipient to authenticate the message
let additional_data = Some(b"additional data".as_slice());
// the sender generate a Nonce and encrypts the message in place
// the encryption method returns the tag/MAC
let mut bytes = message.to_vec();
let nonce = Nonce::new(&mut rng);
let dem = SC::new(&secret_key);
let tag = dem
.encrypt_in_place_detached(&nonce, &mut bytes, additional_data)
.unwrap();
// to transmit the message, the sender can concatenate the nonce, the encrypted data and the MAC
// then send the concatenated result to the recipient
let ciphertext = [nonce.as_bytes(), bytes.as_slice(), tag.as_slice()].concat();
// the ciphertext size is the message size plus the nonce size plus the authentication tag size
assert_eq!(
ciphertext.len(),
message.len() + SC::NONCE_LENGTH + SC::MAC_LENGTH
);
// the recipient extracts the nonce, message and the tag/MAC then decrypt the message in place
let nonce = Nonce::try_from_slice(&ciphertext[..SC::NONCE_LENGTH]).unwrap();
let mut bytes = ciphertext[SC::NONCE_LENGTH..ciphertext.len() - SC::MAC_LENGTH].to_vec();
let tag = ciphertext[ciphertext.len() - SC::MAC_LENGTH..].to_vec();
let dem = SC::new(&secret_key);
dem.decrypt_in_place_detached(&nonce, &mut bytes, &tag, additional_data)
.unwrap();
// assert the decrypted message is identical to the original plaintext
assert_eq!(bytes.as_slice(), message, "Decryption failed");
The library exposes the 2 streaming DEMs offered in the RustCrypto AEAD crate which are based on the authenticated encryption construction as described in the paper Online Authenticated-Encryption and its Nonce-Reuse Misuse-Resistance
use cosmian_crypto_core::XChaCha20Poly1305;
use cosmian_crypto_core::{
reexport::{aead::Payload, rand_core::SeedableRng},
CsRng, DemStream, Instantiable, Nonce, RandomFixedSizeCBytes, SymmetricKey,
};
// Choose one of these streaming DEMs depending on your use case
// type SC = Aes128Gcm;
// type SC = Aes256Gcm;
// type SC = ChaCha20Poly1305;
type SC = XChaCha20Poly1305;
let message = b"Hello, World!";
// The message will be encrypted in 2 chunks, one of size 8 and one of size 5
// In real life, the block size should be much larger and typically a multiple of 4096
const BLOCK_SIZE: usize = 8;
// use some additional data to authenticate the message
let aad = b"the aad";
// generate a random key and nonce
let mut rng = CsRng::from_entropy();
let secret_key = SymmetricKey::new(&mut rng);
let nonce = Nonce::new(&mut rng);
// Instantiate a streaming encryptor
// Two streaming encryptor are available: EncryptorBE32 and EncryptorLE31
// Check the documentation of the DemStream trait for more details
let mut encryptor = SC::new(&secret_key).into_stream_encryptor_be32(&nonce);
// Encrypt the first chunk
// Encryption of all chunks except the last should use `encrypt_next`
let mut ciphertext = encryptor
.encrypt_next(Payload {
msg: &message[..BLOCK_SIZE],
aad,
})
.unwrap();
// Encrypt the second and last chunk using `encrypt_last`
ciphertext.extend_from_slice(
&encryptor
.encrypt_last(Payload {
msg: &message[BLOCK_SIZE..],
aad,
})
.unwrap(),
);
// decryption
// Instantiate a streaming decryptor
let mut decryptor = SC::new(&secret_key).into_stream_decryptor_be32(&nonce);
// Decrypt the first chunk which is BLOCK_SIZE + MAC_LENGTH bytes long
// Decryption of all chunks except the last should use `decrypt_next`
let mut plaintext = decryptor
.decrypt_next(Payload {
msg: &ciphertext[..BLOCK_SIZE + SC::MAC_LENGTH],
aad,
})
.unwrap();
// decrypt the second and last chunk
plaintext.extend_from_slice(
&decryptor
.decrypt_last(Payload {
msg: &ciphertext[BLOCK_SIZE + SC::MAC_LENGTH..],
aad,
})
.unwrap(),
);
assert_eq!(
message.as_slice(),
plaintext.as_slice(),
"Decryption failed"
);
The library exposes 8 ECIES schemes
These use the Dalek implementation of curve 25519.
EciesSalsaSealBox
: which uses the X25519 KEM and the Salsa20 Poly1305 DEM. This scheme is compatible with libsodium
sealed boxes but does not offer support for additional data in the DEM authentication.EciesX25519XChaCha20
: which uses the X25519 KEM and the XChaCha20 Poly1305 DEM; it uses HChaCha for the ephemeral symmetric key derivation and Blake2b for the nonce generation. In case of doubt, this is the recommended scheme.EciesR25519Aes128
: which KEM is based on the Ristretto group of curve 25519 and uses AES 128 GCM as a DEM. Both the derivation of the ephemeral symmetric key and the generation of the nonce is performed using Shake128.EciesX25519Aes128
: which uses the X25519 KEM and uses AES 128 GCM as a DEM. Both the derivation of the ephemeral symmetric key and the generation of the nonce is performed using Shake128.ECIES is supported for the P384, P256, P224 and P192 NIST curves using their RustCrypto implementation. The implementation uses the corresponding curve KEM and the AES 128 GCM DEM. Both the derivation of the ephemeral symmetric key and the generation of the nonce is performed using Shake128.
EciesP384Aes128
EciesP256Aes128
EciesP224Aes128
EciesP192Aes128
The implementations on the curve 25519 offer the best performances, below 100µs for encryption and for decryption on a 2.6GHz Intel Core i7. The implementation on the NIST P256 curve runs at about 500µs for encryption and for decryption, while the P384 implementation runs at about 2ms.
Run the benchmarks to check the performance on your hardware using cargo bench
.
The implementations of the curve 25519 and the P256 curve offer 128 bits of classic security (no post-quantum resistance).
The following example shows how to encrypt a vector of bytes using ECIES X25519 (KEM) combined with XChaCha20 Poly1305 (DEM). It also demonstrates the use of additional data in the DEM authentication.
Encryption is performed using the public key and decryption using the private key.
use cosmian_crypto_core::{
reexport::rand_core::SeedableRng, CsRng, Ecies, EciesX25519XChaCha20,
X25519PrivateKey, X25519PublicKey,
};
// A cryptographic random number generator
let mut rng = CsRng::from_entropy();
// Generate a key pair
let private_key = X25519PrivateKey::new(&mut rng);
let public_key = X25519PublicKey::from(&private_key);
// The plain text to encrypt
let plaintext = b"Hello, World!";
// Some optional authenticated data for theDEM scheme
let authenticated_data = b"Optional authenticated data";
// Encrypt the message with the public key using ECIES X25519 XChaCha20
let ciphertext =
EciesX25519XChaCha20::encrypt(&mut rng, &public_key, plaintext, Some(authenticated_data))
.unwrap();
// Decrypt the message using the private key
let plaintext_ =
EciesX25519XChaCha20::decrypt(&private_key, &ciphertext, Some(authenticated_data)).unwrap();
// Check if the decrypted message is the same as the original message
assert_eq!(plaintext, &plaintext_[..]);
The following example shows how to encrypt a stream of bytes using ECIES X25519 (KEM) combined with XChaCha20 Poly1305 (DEM).
use aead::Payload;
use cosmian_crypto_core::{
reexport::rand_core::SeedableRng, CsRng, EciesStream, EciesX25519XChaCha20,
FixedSizeCBytes, RandomFixedSizeCBytes, X25519PrivateKey, X25519PublicKey,
XChaCha20Poly1305,
};
// generate a random key and nonce
let mut rng = CsRng::from_entropy();
// generate a key pair
let private_key = X25519PrivateKey::new(&mut rng);
let public_key = X25519PublicKey::from(&private_key);
// The plain text to encrypt
let message = b"Hello, World!";
// Some optional authenticated data for theDEM scheme
let authenticated_data = b"Optional authenticated data";
// there will be 2 chunks for the message, one of size 8 and one of size 5
const BLOCK_SIZE: usize = 8;
let (ephemeral_public_key, mut encryptor) =
EciesX25519XChaCha20::get_dem_encryptor_be32(&mut rng, &public_key).unwrap();
// prepend the ciphertext with the ephemeral public key
let mut ciphertext = ephemeral_public_key.to_bytes().to_vec();
// encrypt the first chunk
ciphertext.extend(
encryptor
.encrypt_next(Payload {
msg: &message[..BLOCK_SIZE],
aad: authenticated_data,
})
.unwrap(),
);
// encrypt the second and last chunk
ciphertext.extend_from_slice(
&encryptor
.encrypt_last(Payload {
msg: &message[BLOCK_SIZE..],
aad: authenticated_data,
})
.unwrap(),
);
// decryption
//recover the ephemeral public key from the ciphertext
let ephemeral_public_key =
X25519PublicKey::try_from_slice(&ciphertext[..X25519PublicKey::LENGTH]).unwrap();
// Instantiate a decryptor
let mut decryptor =
EciesX25519XChaCha20::get_dem_decryptor_be32(&private_key, &ephemeral_public_key).unwrap();
// decrypt the first chunk which is BLOCK_SIZE + MAC_LENGTH bytes long
let mut plaintext = decryptor
.decrypt_next(Payload {
msg: &ciphertext[X25519PublicKey::LENGTH
..X25519PublicKey::LENGTH + BLOCK_SIZE + XChaCha20Poly1305::MAC_LENGTH],
aad: authenticated_data,
})
.unwrap();
// decrypt the second and last chunk
plaintext.extend_from_slice(
&decryptor
.decrypt_last(Payload {
msg: &ciphertext
[X25519PublicKey::LENGTH + BLOCK_SIZE + XChaCha20Poly1305::MAC_LENGTH..],
aad: authenticated_data,
})
.unwrap(),
);
assert_eq!(
message.as_slice(),
plaintext.as_slice(),
"Decryption failed"
);
Key wrapping and unwrapping is supported using:
The RFC 5649 key wrapping scheme is implemented using the key_wrap
and Key_unwrap
methods exposed in the key_wrapping_rfc_5649.rs. These methods are compatible with the PKCS#11 CKM_AES_KEY_WRAP_KWP mechanism (which is used in the hybrid RSA-AES key wrapping scheme, see below)
let kek =
b"\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0A\x0B\x0C\x0D\x0E\x0F\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1A\x1B\x1C\x1D\x1E\x1F";
let key_to_wrap =
b"\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0A\x0B\x0C\x0D\x0E\x0F\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1A\x1B\x1C\x1D\x1E\x1F";
let wrapped_key = [
199, 131, 191, 63, 110, 233, 156, 72, 218, 187, 196, 16, 226, 132, 197, 44, 191, 117,
133, 120, 152, 157, 225, 138, 50, 148, 201, 164, 209, 151, 200, 162, 98, 112, 72, 139,
28, 233, 128, 22,
];
assert_eq!(
key_wrap(key_to_wrap, kek).expect("Failed to wrap"),
wrapped_key
);
assert_eq!(
key_unwrap(&wrapped_key, kek).expect("Failed to unwrap"),
key_to_wrap
);
See Ecies above for details on the ECIES schemes.
Various PKCS#11 compatible key wrapping schemes are implemented for RSA keys of size, 2048, 3072 and 4096 bits.
The algorithms are listed below with the corresponding CKM mechanism. Preferably, the Aes256Sha256 algorithm should be used.
Pkcs1v1_5, PKCS #1 v1.5 RS following PKCS#11 CKM_RSA_PKCS The maximum possible plaintext length is m = k - 11, where k is the size of the RSA modulus.
OaepSha256, PKCS #1 RSA with OAEP block format following PKCS#11 CKMRSA_PKCS_OAEP The hash function used is SHA256 The maximum possible plaintext length is m = k - 2 h_len - 2, where k is the size of the RSA modulus and h_len is the size of the hash of the optional label.
OaepSha1, PKCS #1 RSA with OAEP block format following PKCS#11 CKMRSA_PKCS_OAEP The hash function used is SHA1. For that reason this algorithm is not recommended and is only kept here for compatibility with legacy systems. The maximum possible plaintext length is m = k - 2 h_len - 2, where k is the size of the RSA modulus and h_len is the size of the hash of the optional label. This algorithm is compatible with Google Cloud KMS
OaepSha3, PKCS #1 RSA with OAEP block format following PKCS#11 CKM_RSA_PKCS_OAEP The hash function used is SHA3. and is only kept here for compatibility with legacy systems. The maximum possible plaintext length is m = k - 2 * h_len - 2, where k is the size of the RSA modulus and h_len is the size of the hash of the optional label.
Aes256Sha256, Key wrap with AES following PKCS#11 CKM_RSA_AES_KEY_WRAP using an AES key of 256 bits. The hash function used is SHA256. The AES wrapping follows the RFC 5649 which is compatible with PKCS#11 CKM_AES_KEY_WRAP_KWP and there is no limitation on the size of the plaintext (other than those of AES); the recommended plaintext format for an EC Private key is PKCS#8. This algorithm is compatible with Google Cloud KMS
Aes256Sha1, Key wrap with AES following PKCS#11 CKM_RSA_AES_KEY_WRAP using an AES key of 256 bits. The hash function used is SHA1. For that reason this algorithm is not recommended and is only kept here for compatibility with legacy systems. The AES wrapping follows the RFC 5649 which is compatible with PKCS#11 CKM_AES_KEY_WRAP_KWP since there is no limitation on the size of the plaintext; the recommended plaintext format for an EC Private key is PKCS#8. This algorithm is compatible with Google Cloud KMS
Aes256Sha3, Key wrap with AES following PKCS#11 CKM_RSA_AES_KEY_WRAP using an AES key of 256 bits. The hash function used is SHA3-256 (defined in FIPS 202). The AES wrapping follows the RFC 5649 which is compatible with PKCS#11 CKM_AES_KEY_WRAP_KWP since there is no limitation on the size of the plaintext; the recommended plaintext format for an EC Private key is PKCS#8.
use cosmian_crypto_core::{
reexport::rand_core::{RngCore, SeedableRng},
CsRng, RsaKeyLength, RsaKeyWrappingAlgorithm, RsaPrivateKey,
};
use zeroize::Zeroizing;
let mut rng = CsRng::from_entropy();
println!("... Generating a 3072 bit RSA key ...");
let rsa_private_key = RsaPrivateKey::new(&mut rng, RsaKeyLength::Modulus3072).unwrap();
let mut key_to_wrap = [0_u8; 32];
rng.fill_bytes(&mut key_to_wrap);
let mut key_to_wrap = [0_u8; 189];
rng.fill_bytes(&mut key_to_wrap);
let key_to_wrap = Zeroizing::from(key_to_wrap.to_vec());
let rsa_public_key = rsa_private_key.public_key();
print!("Key wrapping with PKCS#11 CKM_RSA_AES_KEY_WRAP SHA-256 AES 256 ...");
let wrapped_key = rsa_public_key
.wrap_key(
&mut rng,
RsaKeyWrappingAlgorithm::Aes256Sha256,
&key_to_wrap,
)
.unwrap();
print!("unwrapping ...: ");
let unwrapped_key = rsa_private_key
.unwrap_key(RsaKeyWrappingAlgorithm::Aes256Sha256, &wrapped_key)
.unwrap();
assert_eq!(unwrapped_key, key_to_wrap);
println!("OK");
The crate currently exposes the EdDSA (Ed25519) signature scheme. More signature schemes will be exposed soon.
Using the static signature implementation, signature and verification are performed in about 50µs on a 2.6 GHz Intel Core i7.
use cosmian_crypto_core::{
reexport::{
rand_core::SeedableRng,
signature::{Signer, Verifier},
},
CsRng, Ed25519PrivateKey, Ed25519PublicKey, RandomFixedSizeCBytes,
};
// instantiate a random number generator
let mut rng = CsRng::from_entropy();
// the message to sign
let message = b"Hello, world!";
// sign the message with the private key
let private_key = Ed25519PrivateKey::new(&mut rng);
let signature = private_key.try_sign(message).unwrap();
// verify the signature with the public key
let public_key = Ed25519PublicKey::from(&private_key);
public_key.verify(message, &signature).unwrap();
Using the cached signature implementation, the signature is performed in about 25µs, verification is still performed in about 50µs on a 2.6 GHz Intel Core i7.
use cosmian_crypto_core::{
reexport::{
rand_core::SeedableRng,
signature::{Signer, Verifier},
},
Cached25519Signer, CsRng, Ed25519PrivateKey, Ed25519PublicKey, RandomFixedSizeCBytes,
};
// instantiate a random number generator
let mut rng = CsRng::from_entropy();
// instantiate the cached signer
let private_key = Ed25519PrivateKey::new(&mut rng);
let cached_signer = Cached25519Signer::try_from(&private_key).unwrap();
// verify the signatures
let public_key = Ed25519PublicKey::from(&private_key);
let message = b"Hello, world!";
let signature = cached_signer.try_sign(message).unwrap();
public_key.verify(message, &signature).unwrap();
let message = b"I'm sorry, Dave. I'm afraid I can't do that.";
let signature = cached_signer.try_sign(message).unwrap();
public_key.verify(message, &signature).unwrap();
The signature API also exposes a Keypair
compatible with that of the RustCrypto implementation.
use cosmian_crypto_core::{
reexport::{
rand_core::SeedableRng,
signature::{Signer, Verifier},
},
CsRng, Ed25519Keypair, FixedSizeCBytes,
};
let mut rng = CsRng::from_entropy();
let message = b"Hello, world!";
// generate a keypair
let keypair = Ed25519Keypair::new(&mut rng);
// serialize the keypair
let serialized_keypair = keypair.to_bytes();
// deserialize the keypair
let keypair = Ed25519Keypair::try_from_bytes(serialized_keypair).unwrap();
//assert equality
assert_eq!(keypair.to_bytes(), serialized_keypair);
// sign the message using the keypair
let signature = keypair.try_sign(message).unwrap();
// verify the signature
keypair.verify(message, &signature).unwrap();
This crate exposes a cryptographically secure random number generator (CS-RNG) that uses the implementation of the CHACHA algorithm with 12 rounds from the rand_chacha
.
It is 128 bits secure.
Performance: 1.7µs per instantiation. The next pseudo-random number is generated in nanoseconds.
use cosmian_crypto_core::CsRng;
let mut rng = CsRng::from_entropy();
This crate uses the pure rust implementation of the SHAKE algorithm from the sha3 crate. Two implementations are available:
kdf128
which is 128-bit secure (in the classic setting) for input sizes of at least 256 bits (32 bytes).kdf256
which is 256-bit secure (in the classic setting) for input sizes of at least 512 bits (64 bytes).Both algorithms run in less than 500ns on a 2.7GHz Intel Core i7.
#[macro_use]
use cosmian_crypto_core::kdf256;
const KEY_LENGTH: usize = 32;
const ikm: &str = "asdf34@!dsa@grq5e$2ASGy5";
// derive a 32-bytes key
let key = kdf256!(KEY_LENGTH, ikm.as_bytes());
assert_eq!(KEY_LENGTH, key.len());
The crate exposes as macros two versions of Blake2 which is specified in RFC 7693
blake2s
: is optimized for 8 to 32-bit platforms and produces digests of any size between 1 and 32 bytes (256 bits)blake2b
: is optimized for 64-bit platforms and produces digests of any size between 1 and 64 bytes (512 bits)Blake2 runs in less than 200ns, which is about 3 times faster than Sha3 and Shake on most hardware but may fail when used to generate hashes of variable length, in particular when the input length is too small.
#[macro_use]
use cosmian_crypto_core::{blakebs, CryptoCoreError};
const LENGTH: usize = 12;
let msg1 = b"asdf34@!dsa@grq5e$2ASGy5";
let msg2 = b"oiu54%6uhg1@34";
let res = blake2b!(LENGTH, msg1, msg2).unwrap();
assert_eq!(LENGTH, res.len());
The documentation can be generated using Cargo:
cargo docs
It is also available on doc.rs.