use std::{borrow::Cow, str::FromStr};
use bitwarden_encoding::{B64, FromStrVisitor};
use coset::{CborSerializable, iana::KeyOperation};
use serde::Deserialize;
use tracing::instrument;
#[cfg(feature = "wasm")]
use wasm_bindgen::convert::{FromWasmAbi, IntoWasmAbi, OptionFromWasmAbi};
use super::{check_length, from_b64, from_b64_vec, split_enc_string};
use crate::{
Aes256CbcHmacKey, ContentFormat, CoseEncrypt0Bytes, KeyDecryptable, KeyEncryptable,
KeyEncryptableWithContentType, SymmetricCryptoKey, Utf8Bytes, XChaCha20Poly1305Key,
cose::XCHACHA20_POLY1305,
error::{CryptoError, EncStringParseError, Result, UnsupportedOperationError},
keys::KeyId,
};
#[cfg(feature = "wasm")]
#[wasm_bindgen::prelude::wasm_bindgen(typescript_custom_section)]
const TS_CUSTOM_TYPES: &'static str = r#"
export type EncString = Tagged<string, "EncString">;
"#;
#[allow(missing_docs)]
#[derive(Clone, zeroize::ZeroizeOnDrop, PartialEq)]
#[allow(unused, non_camel_case_types)]
pub enum EncString {
Aes256Cbc_B64 {
iv: [u8; 16],
data: Vec<u8>,
},
Aes256Cbc_HmacSha256_B64 {
iv: [u8; 16],
mac: [u8; 32],
data: Vec<u8>,
},
Cose_Encrypt0_B64 {
data: Vec<u8>,
},
}
#[cfg(feature = "wasm")]
impl wasm_bindgen::describe::WasmDescribe for EncString {
fn describe() {
<String as wasm_bindgen::describe::WasmDescribe>::describe();
}
}
#[cfg(feature = "wasm")]
impl FromWasmAbi for EncString {
type Abi = <String as FromWasmAbi>::Abi;
unsafe fn from_abi(abi: Self::Abi) -> Self {
use wasm_bindgen::UnwrapThrowExt;
let s = unsafe { String::from_abi(abi) };
Self::from_str(&s).unwrap_throw()
}
}
#[cfg(feature = "wasm")]
impl OptionFromWasmAbi for EncString {
fn is_none(abi: &Self::Abi) -> bool {
<String as OptionFromWasmAbi>::is_none(abi)
}
}
#[cfg(feature = "wasm")]
impl IntoWasmAbi for EncString {
type Abi = <String as IntoWasmAbi>::Abi;
fn into_abi(self) -> Self::Abi {
self.to_string().into_abi()
}
}
impl FromStr for EncString {
type Err = CryptoError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let (enc_type, parts) = split_enc_string(s);
match (enc_type, parts.len()) {
("0", 2) => {
let iv = from_b64(parts[0])?;
let data = from_b64_vec(parts[1])?;
Ok(EncString::Aes256Cbc_B64 { iv, data })
}
("2", 3) => {
let iv = from_b64(parts[0])?;
let data = from_b64_vec(parts[1])?;
let mac = from_b64(parts[2])?;
Ok(EncString::Aes256Cbc_HmacSha256_B64 { iv, mac, data })
}
("7", 1) => {
let buffer = from_b64_vec(parts[0])?;
Ok(EncString::Cose_Encrypt0_B64 { data: buffer })
}
(enc_type, parts) => Err(EncStringParseError::InvalidTypeSymm {
enc_type: enc_type.to_string(),
parts,
}
.into()),
}
}
}
impl EncString {
pub fn try_from_optional(s: Option<String>) -> Result<Option<EncString>, CryptoError> {
s.map(|s| s.parse()).transpose()
}
#[allow(missing_docs)]
pub fn from_buffer(buf: &[u8]) -> Result<Self> {
if buf.is_empty() {
return Err(EncStringParseError::NoType.into());
}
let enc_type = buf[0];
match enc_type {
0 => {
check_length(buf, 18)?;
let iv = buf[1..17].try_into().expect("Valid length");
let data = buf[17..].to_vec();
Ok(EncString::Aes256Cbc_B64 { iv, data })
}
2 => {
check_length(buf, 50)?;
let iv = buf[1..17].try_into().expect("Valid length");
let mac = buf[17..49].try_into().expect("Valid length");
let data = buf[49..].to_vec();
Ok(EncString::Aes256Cbc_HmacSha256_B64 { iv, mac, data })
}
7 => Ok(EncString::Cose_Encrypt0_B64 {
data: buf[1..].to_vec(),
}),
_ => Err(EncStringParseError::InvalidTypeSymm {
enc_type: enc_type.to_string(),
parts: 1,
}
.into()),
}
}
#[allow(missing_docs)]
pub fn to_buffer(&self) -> Result<Vec<u8>> {
let mut buf;
match self {
EncString::Aes256Cbc_B64 { iv, data } => {
buf = Vec::with_capacity(1 + 16 + data.len());
buf.push(self.enc_type());
buf.extend_from_slice(iv);
buf.extend_from_slice(data);
}
EncString::Aes256Cbc_HmacSha256_B64 { iv, mac, data } => {
buf = Vec::with_capacity(1 + 16 + 32 + data.len());
buf.push(self.enc_type());
buf.extend_from_slice(iv);
buf.extend_from_slice(mac);
buf.extend_from_slice(data);
}
EncString::Cose_Encrypt0_B64 { data } => {
buf = Vec::with_capacity(1 + data.len());
buf.push(self.enc_type());
buf.extend_from_slice(data);
}
}
Ok(buf)
}
}
#[allow(clippy::to_string_trait_impl)]
impl ToString for EncString {
fn to_string(&self) -> String {
fn fmt_parts(enc_type: u8, parts: &[&[u8]]) -> String {
let encoded_parts: Vec<String> = parts
.iter()
.map(|part| B64::from(*part).to_string())
.collect();
format!("{}.{}", enc_type, encoded_parts.join("|"))
}
let enc_type = self.enc_type();
match &self {
EncString::Aes256Cbc_B64 { iv, data } => fmt_parts(enc_type, &[iv, data]),
EncString::Aes256Cbc_HmacSha256_B64 { iv, mac, data } => {
fmt_parts(enc_type, &[iv, data, mac])
}
EncString::Cose_Encrypt0_B64 { data } => fmt_parts(enc_type, &[data]),
}
}
}
impl std::fmt::Debug for EncString {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
EncString::Aes256Cbc_B64 { iv, data } => {
let mut debug_struct = f.debug_struct("EncString::Aes256Cbc");
#[cfg(feature = "dangerous-crypto-debug")]
{
debug_struct.field("iv", &hex::encode(iv));
debug_struct.field("data", &hex::encode(data));
}
#[cfg(not(feature = "dangerous-crypto-debug"))]
{
_ = iv;
_ = data;
}
debug_struct.finish()
}
EncString::Aes256Cbc_HmacSha256_B64 { iv, mac, data } => {
let mut debug_struct = f.debug_struct("EncString::Aes256CbcHmacSha256");
#[cfg(feature = "dangerous-crypto-debug")]
{
debug_struct.field("iv", &hex::encode(iv));
debug_struct.field("data", &hex::encode(data));
debug_struct.field("mac", &hex::encode(mac));
}
#[cfg(not(feature = "dangerous-crypto-debug"))]
{
_ = iv;
_ = data;
_ = mac;
}
debug_struct.finish()
}
EncString::Cose_Encrypt0_B64 { data } => {
let mut debug_struct = f.debug_struct("EncString::CoseEncrypt0");
match coset::CoseEncrypt0::from_slice(data.as_slice()) {
Ok(msg) => {
if let Some(ref alg) = msg.protected.header.alg {
let alg_name = match alg {
coset::Algorithm::PrivateUse(XCHACHA20_POLY1305) => {
"XChaCha20-Poly1305"
}
other => return debug_struct.field("algorithm", other).finish(),
};
debug_struct.field("algorithm", &alg_name);
}
let key_id = &msg.protected.header.key_id;
if let Ok(key_id) = KeyId::try_from(key_id.as_slice()) {
debug_struct.field("key_id", &key_id);
}
debug_struct.field("nonce", &hex::encode(msg.unprotected.iv.as_slice()));
if let Some(ref content_type) = msg.protected.header.content_type {
debug_struct.field("content_type", content_type);
}
#[cfg(feature = "dangerous-crypto-debug")]
if let Some(ref ciphertext) = msg.ciphertext {
debug_struct.field("ciphertext", &hex::encode(ciphertext));
}
}
Err(_) => {
debug_struct.field("error", &"INVALID_COSE");
}
}
debug_struct.finish()
}
}
}
}
impl<'de> Deserialize<'de> for EncString {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
deserializer.deserialize_str(FromStrVisitor::new())
}
}
impl serde::Serialize for EncString {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(&self.to_string())
}
}
impl EncString {
pub(crate) fn encrypt_aes256_hmac(
data_dec: &[u8],
key: &Aes256CbcHmacKey,
) -> Result<EncString> {
let (iv, mac, data) =
crate::aes::encrypt_aes256_hmac(data_dec, &key.mac_key, &key.enc_key)?;
Ok(EncString::Aes256Cbc_HmacSha256_B64 { iv, mac, data })
}
pub(crate) fn encrypt_xchacha20_poly1305(
data_dec: &[u8],
key: &XChaCha20Poly1305Key,
content_format: ContentFormat,
) -> Result<EncString> {
let data = crate::cose::encrypt_xchacha20_poly1305(data_dec, key, content_format)?;
Ok(EncString::Cose_Encrypt0_B64 {
data: data.to_vec(),
})
}
const fn enc_type(&self) -> u8 {
match self {
EncString::Aes256Cbc_B64 { .. } => 0,
EncString::Aes256Cbc_HmacSha256_B64 { .. } => 2,
EncString::Cose_Encrypt0_B64 { .. } => 7,
}
}
}
impl KeyEncryptableWithContentType<SymmetricCryptoKey, EncString> for &[u8] {
fn encrypt_with_key(
self,
key: &SymmetricCryptoKey,
content_format: ContentFormat,
) -> Result<EncString> {
match key {
SymmetricCryptoKey::Aes256CbcHmacKey(key) => EncString::encrypt_aes256_hmac(self, key),
SymmetricCryptoKey::XChaCha20Poly1305Key(inner_key) => {
if !inner_key
.supported_operations
.contains(&KeyOperation::Encrypt)
{
return Err(CryptoError::KeyOperationNotSupported(KeyOperation::Encrypt));
}
EncString::encrypt_xchacha20_poly1305(self, inner_key, content_format)
}
SymmetricCryptoKey::Aes256CbcKey(_) => Err(CryptoError::OperationNotSupported(
UnsupportedOperationError::EncryptionNotImplementedForKey,
)),
}
}
}
impl KeyDecryptable<SymmetricCryptoKey, Vec<u8>> for EncString {
fn decrypt_with_key(&self, key: &SymmetricCryptoKey) -> Result<Vec<u8>> {
match (self, key) {
(EncString::Aes256Cbc_B64 { iv, data }, SymmetricCryptoKey::Aes256CbcKey(key)) => {
crate::aes::decrypt_aes256(iv, data.clone(), &key.enc_key)
.map_err(|_| CryptoError::Decrypt)
}
(
EncString::Aes256Cbc_HmacSha256_B64 { iv, mac, data },
SymmetricCryptoKey::Aes256CbcHmacKey(key),
) => crate::aes::decrypt_aes256_hmac(iv, mac, data.clone(), &key.mac_key, &key.enc_key)
.map_err(|_| CryptoError::Decrypt),
(
EncString::Cose_Encrypt0_B64 { data },
SymmetricCryptoKey::XChaCha20Poly1305Key(key),
) => {
let (decrypted_message, _) = crate::cose::decrypt_xchacha20_poly1305(
&CoseEncrypt0Bytes::from(data.as_slice()),
key,
)?;
Ok(decrypted_message)
}
_ => Err(CryptoError::WrongKeyType),
}
}
}
impl KeyEncryptable<SymmetricCryptoKey, EncString> for String {
fn encrypt_with_key(self, key: &SymmetricCryptoKey) -> Result<EncString> {
Utf8Bytes::from(self).encrypt_with_key(key)
}
}
impl KeyEncryptable<SymmetricCryptoKey, EncString> for &str {
fn encrypt_with_key(self, key: &SymmetricCryptoKey) -> Result<EncString> {
Utf8Bytes::from(self).encrypt_with_key(key)
}
}
impl KeyDecryptable<SymmetricCryptoKey, String> for EncString {
#[instrument(err, skip_all)]
fn decrypt_with_key(&self, key: &SymmetricCryptoKey) -> Result<String> {
let dec: Vec<u8> = self.decrypt_with_key(key)?;
String::from_utf8(dec).map_err(|_| CryptoError::InvalidUtf8String)
}
}
impl schemars::JsonSchema for EncString {
fn schema_name() -> Cow<'static, str> {
"EncString".into()
}
fn json_schema(generator: &mut schemars::generate::SchemaGenerator) -> schemars::Schema {
generator.subschema_for::<String>()
}
}
#[cfg(test)]
mod tests {
use coset::iana::KeyOperation;
use schemars::schema_for;
use super::EncString;
use crate::{
CryptoError, KEY_ID_SIZE, KeyDecryptable, KeyEncryptable, SymmetricCryptoKey,
derive_symmetric_key,
};
fn encrypt_with_xchacha20(plaintext: &str) -> EncString {
let key_id = [0u8; KEY_ID_SIZE];
let enc_key = [0u8; 32];
let key = SymmetricCryptoKey::XChaCha20Poly1305Key(crate::XChaCha20Poly1305Key {
key_id: key_id.into(),
enc_key: Box::pin(enc_key.into()),
supported_operations: vec![
coset::iana::KeyOperation::Decrypt,
coset::iana::KeyOperation::Encrypt,
coset::iana::KeyOperation::WrapKey,
coset::iana::KeyOperation::UnwrapKey,
],
});
plaintext.encrypt_with_key(&key).expect("encryption works")
}
#[test]
#[ignore = "Manual test to verify debug format"]
fn test_debug() {
let enc_string = encrypt_with_xchacha20("Test debug string");
println!("{:?}", enc_string);
let enc_string_aes =
EncString::encrypt_aes256_hmac(b"Test debug string", &derive_symmetric_key("test"))
.unwrap();
println!("{:?}", enc_string_aes);
}
#[test]
fn test_xchacha20_encstring_string_padding_block_sizes() {
let cases = [
("", 32), (&"a".repeat(31), 32), (&"a".repeat(32), 64), (&"a".repeat(63), 64), (&"a".repeat(64), 96), ];
let ciphertext_lengths: Vec<_> = cases
.iter()
.map(|(plaintext, _)| encrypt_with_xchacha20(plaintext).to_string().len())
.collect();
assert_eq!(ciphertext_lengths[0], ciphertext_lengths[1]);
assert_ne!(ciphertext_lengths[1], ciphertext_lengths[2]);
assert_eq!(ciphertext_lengths[2], ciphertext_lengths[3]);
assert_ne!(ciphertext_lengths[3], ciphertext_lengths[4]);
}
#[test]
fn test_enc_roundtrip_xchacha20() {
let key_id = [0u8; KEY_ID_SIZE];
let enc_key = [0u8; 32];
let key = SymmetricCryptoKey::XChaCha20Poly1305Key(crate::XChaCha20Poly1305Key {
key_id: key_id.into(),
enc_key: Box::pin(enc_key.into()),
supported_operations: vec![
coset::iana::KeyOperation::Decrypt,
coset::iana::KeyOperation::Encrypt,
coset::iana::KeyOperation::WrapKey,
coset::iana::KeyOperation::UnwrapKey,
],
});
let test_string = "encrypted_test_string";
let cipher = test_string.to_owned().encrypt_with_key(&key).unwrap();
let decrypted_str: String = cipher.decrypt_with_key(&key).unwrap();
assert_eq!(decrypted_str, test_string);
}
#[test]
fn test_enc_string_roundtrip() {
let key = SymmetricCryptoKey::Aes256CbcHmacKey(derive_symmetric_key("test"));
let test_string = "encrypted_test_string";
let cipher = test_string.to_string().encrypt_with_key(&key).unwrap();
let decrypted_str: String = cipher.decrypt_with_key(&key).unwrap();
assert_eq!(decrypted_str, test_string);
}
#[test]
fn test_enc_roundtrip_xchacha20_empty() {
let key_id = [0u8; KEY_ID_SIZE];
let enc_key = [0u8; 32];
let key = SymmetricCryptoKey::XChaCha20Poly1305Key(crate::XChaCha20Poly1305Key {
key_id: key_id.into(),
enc_key: Box::pin(enc_key.into()),
supported_operations: vec![
coset::iana::KeyOperation::Decrypt,
coset::iana::KeyOperation::Encrypt,
coset::iana::KeyOperation::WrapKey,
coset::iana::KeyOperation::UnwrapKey,
],
});
let test_string = "";
let cipher = test_string.to_owned().encrypt_with_key(&key).unwrap();
let decrypted_str: String = cipher.decrypt_with_key(&key).unwrap();
assert_eq!(decrypted_str, test_string);
}
#[test]
fn test_enc_string_roundtrip_empty() {
let key = SymmetricCryptoKey::Aes256CbcHmacKey(derive_symmetric_key("test"));
let test_string = "";
let cipher = test_string.to_string().encrypt_with_key(&key).unwrap();
let decrypted_str: String = cipher.decrypt_with_key(&key).unwrap();
assert_eq!(decrypted_str, test_string);
}
#[test]
fn test_enc_string_ref_roundtrip() {
let key = SymmetricCryptoKey::Aes256CbcHmacKey(derive_symmetric_key("test"));
let test_string: &'static str = "encrypted_test_string";
let cipher = test_string.to_string().encrypt_with_key(&key).unwrap();
let decrypted_str: String = cipher.decrypt_with_key(&key).unwrap();
assert_eq!(decrypted_str, test_string);
}
#[test]
fn test_enc_string_serialization() {
#[derive(serde::Serialize, serde::Deserialize)]
struct Test {
key: EncString,
}
let cipher = "2.pMS6/icTQABtulw52pq2lg==|XXbxKxDTh+mWiN1HjH2N1w==|Q6PkuT+KX/axrgN9ubD5Ajk2YNwxQkgs3WJM0S0wtG8=";
let serialized = format!("{{\"key\":\"{cipher}\"}}");
let t = serde_json::from_str::<Test>(&serialized).unwrap();
assert_eq!(t.key.enc_type(), 2);
assert_eq!(t.key.to_string(), cipher);
assert_eq!(serde_json::to_string(&t).unwrap(), serialized);
}
#[test]
fn test_enc_from_to_buffer() {
let enc_str: &str = "2.pMS6/icTQABtulw52pq2lg==|XXbxKxDTh+mWiN1HjH2N1w==|Q6PkuT+KX/axrgN9ubD5Ajk2YNwxQkgs3WJM0S0wtG8=";
let enc_string: EncString = enc_str.parse().unwrap();
let enc_buf = enc_string.to_buffer().unwrap();
assert_eq!(
enc_buf,
vec![
2, 164, 196, 186, 254, 39, 19, 64, 0, 109, 186, 92, 57, 218, 154, 182, 150, 67,
163, 228, 185, 63, 138, 95, 246, 177, 174, 3, 125, 185, 176, 249, 2, 57, 54, 96,
220, 49, 66, 72, 44, 221, 98, 76, 209, 45, 48, 180, 111, 93, 118, 241, 43, 16, 211,
135, 233, 150, 136, 221, 71, 140, 125, 141, 215
]
);
let enc_string_new = EncString::from_buffer(&enc_buf).unwrap();
assert_eq!(enc_string_new.to_string(), enc_str)
}
#[test]
fn test_from_str_cbc256() {
let enc_str = "0.pMS6/icTQABtulw52pq2lg==|XXbxKxDTh+mWiN1HjH2N1w==";
let enc_string: EncString = enc_str.parse().unwrap();
assert_eq!(enc_string.enc_type(), 0);
if let EncString::Aes256Cbc_B64 { iv, data } = &enc_string {
assert_eq!(
iv,
&[
164, 196, 186, 254, 39, 19, 64, 0, 109, 186, 92, 57, 218, 154, 182, 150
]
);
assert_eq!(
data,
&[
93, 118, 241, 43, 16, 211, 135, 233, 150, 136, 221, 71, 140, 125, 141, 215
]
);
} else {
panic!("Invalid variant")
};
}
#[test]
fn test_decrypt_cbc256() {
let key = "hvBMMb1t79YssFZkpetYsM3deyVuQv4r88Uj9gvYe08=".to_string();
let key = SymmetricCryptoKey::try_from(key).unwrap();
let enc_str = "0.NQfjHLr6za7VQVAbrpL81w==|wfrjmyJ0bfwkQlySrhw8dA==";
let enc_string: EncString = enc_str.parse().unwrap();
assert_eq!(enc_string.enc_type(), 0);
let dec_str: String = enc_string.decrypt_with_key(&key).unwrap();
assert_eq!(dec_str, "EncryptMe!");
}
#[test]
fn test_decrypt_downgrade_encstring_prevention() {
let key = "hvBMMb1t79YssFZkpetYsM3deyVuQv4r88Uj9gvYe0+G8EwxvW3v1iywVmSl61iwzd17JW5C/ivzxSP2C9h7Tw==".to_string();
let key = SymmetricCryptoKey::try_from(key).unwrap();
let enc_str = "0.NQfjHLr6za7VQVAbrpL81w==|wfrjmyJ0bfwkQlySrhw8dA==";
let enc_string: EncString = enc_str.parse().unwrap();
assert_eq!(enc_string.enc_type(), 0);
let result: Result<String, CryptoError> = enc_string.decrypt_with_key(&key);
assert!(matches!(result, Err(CryptoError::WrongKeyType)));
}
#[test]
fn test_encrypt_fails_when_operation_not_allowed() {
let key_id = [0u8; KEY_ID_SIZE];
let enc_key = [0u8; 32];
let key = SymmetricCryptoKey::XChaCha20Poly1305Key(crate::XChaCha20Poly1305Key {
key_id: key_id.into(),
enc_key: Box::pin(enc_key.into()),
supported_operations: vec![KeyOperation::Decrypt],
});
let plaintext = "should fail";
let result = plaintext.encrypt_with_key(&key);
assert!(
matches!(
result,
Err(CryptoError::KeyOperationNotSupported(KeyOperation::Encrypt))
),
"Expected encrypt to fail with KeyOperationNotSupported, got: {result:?}"
);
}
#[test]
fn test_from_str_invalid() {
let enc_str = "8.ABC";
let enc_string: Result<EncString, _> = enc_str.parse();
let err = enc_string.unwrap_err();
assert_eq!(
err.to_string(),
"EncString error, Invalid symmetric type, got type 8 with 1 parts"
);
}
#[test]
#[cfg(not(feature = "dangerous-crypto-debug"))]
fn test_debug_format() {
let enc_str = "2.pMS6/icTQABtulw52pq2lg==|XXbxKxDTh+mWiN1HjH2N1w==|Q6PkuT+KX/axrgN9ubD5Ajk2YNwxQkgs3WJM0S0wtG8=";
let enc_string: EncString = enc_str.parse().unwrap();
assert_eq!(
"EncString::Aes256CbcHmacSha256".to_string(),
format!("{:?}", enc_string)
);
}
#[test]
fn test_json_schema() {
let schema = schema_for!(EncString);
assert_eq!(
serde_json::to_string(&schema).unwrap(),
r#"{"$schema":"https://json-schema.org/draft/2020-12/schema","title":"EncString","type":"string"}"#
);
}
}