use crate::{Chacha, Cipher, Error};
use base64::{DecodeError, Engine as _, engine::general_purpose::STANDARD_NO_PAD as b64};
use serde::{Deserialize, Serialize};
pub struct ChachaB64 {
chacha: Chacha,
}
impl ChachaB64 {
#[must_use]
pub fn with_pbkdf2_rounds(pbkdf_rounds: u32) -> Self {
Self {
chacha: Chacha::with_pbkdf2_rounds(pbkdf_rounds),
}
}
pub fn encrypt(&self, secret: &[u8], pw: &str) -> Result<CipherB64<String, String>, Error> {
Ok(self.chacha.encrypt(secret, pw)?.into())
}
#[allow(clippy::missing_panics_doc)]
pub fn encrypt_auth(
&self,
secret: &[u8],
auth: &[u8],
pw: &str,
) -> Result<CipherB64<String, String>, Error> {
Ok(self.chacha.encrypt_auth(secret, auth, pw)?.into())
}
#[allow(clippy::missing_panics_doc)]
pub fn decrypt(&self, cipher_b64: CipherB64<&str, &str>, pw: &str) -> Result<Vec<u8>, Error> {
self.chacha.decrypt(
&cipher_b64
.try_into()
.map_err(|e: DecodeError| Error::Decode(e.to_string()))?,
pw,
)
}
#[allow(clippy::missing_panics_doc)]
pub fn decrypt_auth(
&self,
cipher_b64: CipherB64<&str, &str>,
auth: &[u8],
pw: &str,
) -> Result<Vec<u8>, Error> {
self.chacha.decrypt_auth(
&cipher_b64
.try_into()
.map_err(|e: DecodeError| Error::Decode(e.to_string()))?,
auth,
pw,
)
}
}
#[derive(Serialize, Deserialize)]
pub struct CipherB64<S1, S2> {
pub salt: S1,
pub ciphertext: S2,
pub nonce: S2,
}
impl CipherB64<&str, &str> {
#[must_use]
pub fn parse(s: &str) -> Option<CipherB64<&str, &str>> {
let mut cc_it = s.split(&[':']);
match (cc_it.next(), cc_it.next(), cc_it.next()) {
(Some(salt), Some(ciphertext), Some(nonce)) => Some(CipherB64::<&str, &str> {
salt,
ciphertext,
nonce,
}),
_ => None,
}
}
}
impl std::fmt::Display for CipherB64<String, String> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}:{}:{}", &self.salt, &self.ciphertext, &self.nonce)
}
}
impl std::convert::From<Cipher> for CipherB64<String, String> {
fn from(cipher: Cipher) -> Self {
CipherB64 {
salt: cipher.salt,
ciphertext: b64.encode(cipher.ciphertext),
nonce: b64.encode(cipher.nonce),
}
}
}
impl std::convert::TryFrom<CipherB64<&str, &str>> for Cipher {
type Error = DecodeError;
fn try_from(cipher_b64: CipherB64<&str, &str>) -> Result<Self, Self::Error> {
Ok(Cipher {
salt: cipher_b64.salt.to_string(),
ciphertext: b64.decode(cipher_b64.ciphertext.as_bytes())?,
nonce: b64.decode(cipher_b64.nonce.as_bytes())?,
})
}
}
#[cfg(test)]
mod test {
use super::{ChachaB64, CipherB64};
use serde::{Deserialize, Serialize};
#[test]
fn test_encrypt_decrypt() {
let test_data = "daewörjlser,dk gjxlre.t98i1df.lskejr lewiri23r9ß iu4ötirjf";
let pw = "LOIUo98zkjhB";
let cc64 = ChachaB64::with_pbkdf2_rounds(145_654);
let cipher_b64 = cc64.encrypt(test_data.as_bytes(), pw).unwrap();
let cipher_b64_2 = CipherB64::<&str, &str> {
salt: &cipher_b64.salt,
ciphertext: &cipher_b64.ciphertext,
nonce: &cipher_b64.nonce,
};
let test_data2 = String::from_utf8(cc64.decrypt(cipher_b64_2, pw).unwrap()).unwrap();
assert_eq!(test_data, &test_data2);
}
#[test]
fn test_with_auth() {
#[derive(Serialize, Deserialize)]
struct FileStruct {
comment: String,
description: String,
encrypted_secret: String,
}
const COMMENT: &str = "# Dont' modify this file";
let cc64 = ChachaB64::with_pbkdf2_rounds(145_654);
let secret = vec!["elephant", "mouse", "bee", "swift"];
let description = format!("{} mammals, {} insect, {} bird", 2, 1, 1);
let pw = "LOIUo98zkjhB";
let encrypted_secret = cc64
.encrypt_auth(
serde_json::to_string(&secret).unwrap().as_bytes(),
description.as_bytes(),
pw,
)
.unwrap();
let file_content = FileStruct {
comment: COMMENT.to_string(),
description,
encrypted_secret: encrypted_secret.to_string(),
};
let serialized_file_content = serde_json::to_string_pretty(&file_content).unwrap();
assert_eq!(
serialized_file_content[0..114],
r##"{
"comment": "# Dont' modify this file",
"description": "2 mammals, 1 insect, 1 bird",
"encrypted_secret": "NkJ3RDl3UWJ5UWZPdEVOR2tKdHJBdw:2FeIy66BS8HFFDpX6+gNs6wf3gVhIii2QtMG2Bm1tfH1OubbMIEYMl5ZmYzUhBzykLEAQWXThmM0ayjCDWp"
}"##[0..114]
);
let file_content2: FileStruct = serde_json::from_str(&serialized_file_content).unwrap();
let secret2: Vec<String> = serde_json::from_str(&String::from_utf8_lossy(
&cc64
.decrypt_auth(
CipherB64::parse(&file_content2.encrypted_secret).unwrap(),
file_content2.description.as_bytes(),
pw,
)
.unwrap(),
))
.unwrap();
assert_eq!(secret, secret2);
}
}