obnam 0.8.0

a backup program
Documentation
//! Encryption cipher algorithms.

use crate::chunk::DataChunk;
use crate::chunkmeta::ChunkMeta;
use crate::passwords::Passwords;

use aes_gcm::aead::{generic_array::GenericArray, Aead, NewAead, Payload};
use aes_gcm::Aes256Gcm; // Or `Aes128Gcm`
use rand::Rng;

use std::str::FromStr;

const CHUNK_V1: &[u8] = b"0001";

/// An encrypted chunk.
///
/// This consists of encrypted ciphertext, and un-encrypted (or
/// cleartext) additional associated data, which could be the metadata
/// of the chunk, and be used to, for example, find chunks.
///
/// Encrypted chunks are the only chunks that can be uploaded to the
/// server.
pub struct EncryptedChunk {
    ciphertext: Vec<u8>,
    aad: Vec<u8>,
}

impl EncryptedChunk {
    /// Create an encrypted chunk.
    fn new(ciphertext: Vec<u8>, aad: Vec<u8>) -> Self {
        Self { ciphertext, aad }
    }

    /// Return the encrypted data.
    pub fn ciphertext(&self) -> &[u8] {
        &self.ciphertext
    }

    /// Return the cleartext associated additional data.
    pub fn aad(&self) -> &[u8] {
        &self.aad
    }
}

/// An engine for encrypting and decrypting chunks.
pub struct CipherEngine {
    cipher: Aes256Gcm,
}

impl CipherEngine {
    /// Create a new cipher engine using cleartext passwords.
    pub fn new(pass: &Passwords) -> Self {
        let key = GenericArray::from_slice(pass.encryption_key());
        Self {
            cipher: Aes256Gcm::new(key),
        }
    }

    /// Encrypt a chunk.
    pub fn encrypt_chunk(&self, chunk: &DataChunk) -> Result<EncryptedChunk, CipherError> {
        // Payload with metadata as associated data, to be encrypted.
        //
        // The metadata will be stored in cleartext after encryption.
        let aad = chunk.meta().to_json_vec();
        let payload = Payload {
            msg: chunk.data(),
            aad: &aad,
        };

        // Unique random key for each encryption.
        let nonce = Nonce::new();
        let nonce_arr = GenericArray::from_slice(nonce.as_bytes());

        // Encrypt the sensitive part.
        let ciphertext = self
            .cipher
            .encrypt(nonce_arr, payload)
            .map_err(CipherError::EncryptError)?;

        // Construct the blob to be stored on the server.
        let mut vec: Vec<u8> = vec![];
        push_bytes(&mut vec, CHUNK_V1);
        push_bytes(&mut vec, nonce.as_bytes());
        push_bytes(&mut vec, &ciphertext);

        Ok(EncryptedChunk::new(vec, aad))
    }

    /// Decrypt a chunk.
    pub fn decrypt_chunk(&self, bytes: &[u8], meta: &[u8]) -> Result<DataChunk, CipherError> {
        // Does encrypted chunk start with the right version?
        if !bytes.starts_with(CHUNK_V1) {
            return Err(CipherError::UnknownChunkVersion);
        }
        let version_len = CHUNK_V1.len();
        let bytes = &bytes[version_len..];

        let (nonce, ciphertext) = match bytes.get(..NONCE_SIZE) {
            Some(nonce) => (GenericArray::from_slice(nonce), &bytes[NONCE_SIZE..]),
            None => return Err(CipherError::NoNonce),
        };

        let payload = Payload {
            msg: ciphertext,
            aad: meta,
        };

        let payload = self
            .cipher
            .decrypt(nonce, payload)
            .map_err(CipherError::DecryptError)?;
        let payload = Payload::from(payload.as_slice());

        let meta = std::str::from_utf8(meta)?;
        let meta = ChunkMeta::from_str(meta)?;

        let chunk = DataChunk::new(payload.msg.to_vec(), meta);

        Ok(chunk)
    }
}

fn push_bytes(vec: &mut Vec<u8>, bytes: &[u8]) {
    for byte in bytes.iter() {
        vec.push(*byte);
    }
}

/// Possible errors when encrypting or decrypting chunks.
#[derive(Debug, thiserror::Error)]
pub enum CipherError {
    /// Encryption failed.
    #[error("failed to encrypt with AES-GEM: {0}")]
    EncryptError(aes_gcm::Error),

    /// The encrypted chunk has an unsupported version or is
    /// corrupted.
    #[error("encrypted chunk does not start with correct version")]
    UnknownChunkVersion,

    /// The encrypted chunk lacks a complete nonce value, and is
    /// probably corrupted.
    #[error("encrypted chunk does not have a complete nonce")]
    NoNonce,

    /// Decryption failed.
    #[error("failed to decrypt with AES-GEM: {0}")]
    DecryptError(aes_gcm::Error),

    /// The decryption succeeded, by data isn't valid YAML.
    #[error("failed to parse decrypted data as a DataChunk: {0}")]
    Parse(serde_yaml::Error),

    /// Error parsing UTF8 data.
    #[error(transparent)]
    Utf8Error(#[from] std::str::Utf8Error),

    /// Error parsing JSON data.
    #[error("failed to parse JSON: {0}")]
    JsonParse(#[from] serde_json::Error),
}

const NONCE_SIZE: usize = 12;

#[derive(Debug)]
struct Nonce {
    nonce: Vec<u8>,
}

impl Nonce {
    fn from_bytes(bytes: &[u8]) -> Self {
        assert_eq!(bytes.len(), NONCE_SIZE);
        Self {
            nonce: bytes.to_vec(),
        }
    }

    fn new() -> Self {
        let mut bytes: Vec<u8> = vec![0; NONCE_SIZE];
        let mut rng = rand::thread_rng();
        for x in bytes.iter_mut() {
            *x = rng.gen();
        }
        Self::from_bytes(&bytes)
    }

    fn as_bytes(&self) -> &[u8] {
        &self.nonce
    }
}

#[cfg(test)]
mod test {
    use crate::chunk::DataChunk;
    use crate::chunkmeta::ChunkMeta;
    use crate::cipher::{CipherEngine, CipherError, CHUNK_V1, NONCE_SIZE};
    use crate::label::Label;
    use crate::passwords::Passwords;

    #[test]
    fn metadata_as_aad() {
        let sum = Label::sha256(b"dummy data");
        let meta = ChunkMeta::new(&sum);
        let meta_as_aad = meta.to_json_vec();
        let chunk = DataChunk::new("hello".as_bytes().to_vec(), meta);
        let pass = Passwords::new("secret");
        let cipher = CipherEngine::new(&pass);
        let enc = cipher.encrypt_chunk(&chunk).unwrap();

        assert_eq!(meta_as_aad, enc.aad());
    }

    #[test]
    fn round_trip() {
        let sum = Label::sha256(b"dummy data");
        let meta = ChunkMeta::new(&sum);
        let chunk = DataChunk::new("hello".as_bytes().to_vec(), meta);
        let pass = Passwords::new("secret");

        let cipher = CipherEngine::new(&pass);
        let enc = cipher.encrypt_chunk(&chunk).unwrap();

        let bytes: Vec<u8> = enc.ciphertext().to_vec();
        let dec = cipher.decrypt_chunk(&bytes, enc.aad()).unwrap();
        assert_eq!(chunk, dec);
    }

    #[test]
    fn decrypt_errors_if_nonce_is_too_short() {
        let pass = Passwords::new("our little test secret");
        let e = CipherEngine::new(&pass);

        // *Almost* a valid chunk header, except it's one byte too short
        let bytes = {
            let mut result = [0; CHUNK_V1.len() + NONCE_SIZE - 1];
            for (i, x) in CHUNK_V1.iter().enumerate() {
                result[i] = *x;
            }
            result
        };

        let meta = [0; 0];

        assert!(matches!(
            e.decrypt_chunk(&bytes, &meta),
            Err(CipherError::NoNonce)
        ));
    }
}