use cbc::cipher::{KeyIvInit, StreamCipher};
use rand_core::{OsRng, RngCore};
use salsa20::Salsa20;
use crate::contract::{Identified, SymmetricCipher};
use crate::error::Result;
use crate::internal::etm;
use crate::key::Key;
const LABEL: &str = "Salsa20";
const NONCE_LEN: usize = 8;
#[derive(Debug)]
pub struct Salsa20Encryptor {
key: Key<32>,
}
impl Salsa20Encryptor {
pub const IDENTIFIER: &'static str = "salsa20";
pub fn new(key: [u8; 32]) -> Self {
Self { key: Key::new(key) }
}
pub fn from_slice(key: &[u8]) -> Result<Self> {
Ok(Self {
key: Key::from_slice(key, LABEL)?,
})
}
}
impl Identified for Salsa20Encryptor {
fn identifier(&self) -> &str {
Self::IDENTIFIER
}
}
impl SymmetricCipher for Salsa20Encryptor {
fn encrypt(&self, plaintext: &[u8]) -> Result<Vec<u8>> {
let mut nonce = [0u8; NONCE_LEN];
OsRng.fill_bytes(&mut nonce);
let mut buffer = plaintext.to_vec();
Salsa20::new_from_slices(self.key.as_bytes(), &nonce)
.expect("key and nonce lengths are fixed")
.apply_keystream(&mut buffer);
Ok(etm::pack(&nonce, &buffer, self.key.as_bytes()))
}
fn decrypt(&self, ciphertext: &[u8]) -> Result<Vec<u8>> {
let (nonce, body) =
etm::unpack_and_verify(ciphertext, NONCE_LEN, self.key.as_bytes(), LABEL)?;
let mut buffer = body.to_vec();
Salsa20::new_from_slices(self.key.as_bytes(), nonce)
.expect("key and nonce lengths were verified")
.apply_keystream(&mut buffer);
Ok(buffer)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::error::Error;
fn unhex(s: &str) -> Vec<u8> {
(0..s.len() / 2)
.map(|i| u8::from_str_radix(&s[2 * i..2 * i + 2], 16).unwrap())
.collect()
}
#[test]
fn bare_keystream_matches_estream_vectors() {
let key = unhex("8000000000000000000000000000000000000000000000000000000000000000");
let nonce = unhex("0000000000000000");
let mut keystream = [0u8; 64];
Salsa20::new_from_slices(&key, &nonce)
.unwrap()
.apply_keystream(&mut keystream);
assert_eq!(
keystream.as_slice(),
unhex(concat!(
"e3be8fdd8beca2e3ea8ef9475b29a6e7",
"003951e1097a5c38d23b7a5fad9f6844",
"b22c97559e2723c7cbbd3fe4fc8d9a07",
"44652a83e72a9c461876af4d7ef1a117"
))
.as_slice()
);
let key = unhex("0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20");
let nonce = unhex("0301040105090206");
let mut keystream = [0u8; 256];
Salsa20::new_from_slices(&key, &nonce)
.unwrap()
.apply_keystream(&mut keystream);
assert_eq!(
keystream.as_slice(),
unhex(concat!(
"6ebcbdbf76fccc64ab05542bee8a67cb",
"c28fa2e141fbefbb3a2f9b221909c8d7",
"d4295258cb539770dd24d7ac3443769f",
"fa27a50e60644264dc8b6b612683372e",
"085d0a12bf240b189ce2b78289862b56",
"fdc9fcffc33bef9325a2e81b98fb3fb9",
"aa04cf434615ceffeb985c1cb08d8440",
"e90b1d56ddeaea16d9e15affff1f698c",
"483c7a466af1fe062574adfd2b06a62b",
"4d98440719ea776385c470349a7ed696",
"9583463ed5d26b8fefccb205da0f5bfa",
"98c77812fe756b09eacc282aa42f4baf",
"a79633189046e2b20f35b3e0e54aa3b9",
"29e23c0f47dc7bcd4f928b2a9764be7d",
"4b8a50f980a50b35ad8087375e0c556e",
"cbe6a7161e8653ce9391e1e6710ed4f1"
))
.as_slice()
);
}
#[test]
fn roundtrip_including_multi_block_and_empty() {
let encryptor = Salsa20Encryptor::new([3u8; 32]);
for plaintext in [b"".as_slice(), b"a", b"abc", b"abcd", &[0xffu8; 200]] {
let blob = encryptor.encrypt(plaintext).unwrap();
assert_eq!(encryptor.decrypt(&blob).unwrap(), plaintext);
}
}
#[test]
fn ciphertext_differs_from_plaintext_and_tamper_fails() {
let encryptor = Salsa20Encryptor::new([3u8; 32]);
let blob = encryptor.encrypt(b"0123456789").unwrap();
assert_ne!(&blob[42..], b"0123456789");
let mut tampered = blob.clone();
let last = tampered.len() - 1;
tampered[last] ^= 0x01;
assert_eq!(
encryptor.decrypt(&tampered).unwrap_err(),
Error::MacVerificationFailed { label: LABEL }
);
}
#[test]
fn wrong_key_fails_mac() {
let blob = Salsa20Encryptor::new([3u8; 32]).encrypt(b"secret").unwrap();
let other = Salsa20Encryptor::new([4u8; 32]);
assert_eq!(
other.decrypt(&blob).unwrap_err(),
Error::MacVerificationFailed { label: LABEL }
);
}
#[test]
fn nonce_is_fresh_per_encryption() {
let encryptor = Salsa20Encryptor::new([3u8; 32]);
let first = encryptor.encrypt(b"same plaintext").unwrap();
let second = encryptor.encrypt(b"same plaintext").unwrap();
assert_ne!(first, second);
}
#[test]
fn invalid_prefix_and_too_short_are_rejected() {
let encryptor = Salsa20Encryptor::new([3u8; 32]);
assert_eq!(
encryptor.decrypt(b"v9 not a blob").unwrap_err(),
Error::InvalidPrefix { label: LABEL }
);
assert_eq!(
encryptor.decrypt(b"v1 short").unwrap_err(),
Error::TooShort { label: LABEL }
);
}
#[test]
fn key_length_is_checked() {
assert_eq!(
Salsa20Encryptor::from_slice(&[0u8; 16]).unwrap_err(),
Error::InvalidKeyLength {
label: LABEL,
expected: 32,
got: 16
}
);
}
}