summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLars Wirzenius <liw@liw.fi>2021-05-29 11:39:52 +0300
committerLars Wirzenius <liw@liw.fi>2021-05-31 16:40:32 +0300
commit4e95e1003c4f2c89a807977b34f287d2f200f5bc (patch)
tree8f1ba3e773f5691742a26ca57799da2f834bcceb
parent426e6acb7129756aea295c01b56a902d655efa6d (diff)
downloadobnam2-4e95e1003c4f2c89a807977b34f287d2f200f5bc.tar.gz
feat: add chunk encryption
-rw-r--r--src/cipher.rs200
-rw-r--r--src/error.rs4
-rw-r--r--src/lib.rs1
3 files changed, 205 insertions, 0 deletions
diff --git a/src/cipher.rs b/src/cipher.rs
new file mode 100644
index 0000000..550fafd
--- /dev/null
+++ b/src/cipher.rs
@@ -0,0 +1,200 @@
+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";
+
+pub struct EncryptedChunk {
+ ciphertext: Vec<u8>,
+ aad: Vec<u8>,
+}
+
+impl EncryptedChunk {
+ fn new(ciphertext: Vec<u8>, aad: Vec<u8>) -> Self {
+ Self { ciphertext, aad }
+ }
+
+ pub fn ciphertext(&self) -> &[u8] {
+ &self.ciphertext
+ }
+
+ pub fn aad(&self) -> &[u8] {
+ &self.aad
+ }
+}
+
+pub struct CipherEngine {
+ cipher: Aes256Gcm,
+}
+
+impl CipherEngine {
+ pub fn new(pass: &Passwords) -> Self {
+ let key = GenericArray::from_slice(pass.encryption_key());
+ Self {
+ cipher: Aes256Gcm::new(key),
+ }
+ }
+
+ 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))
+ }
+
+ 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..];
+
+ // Get nonce.
+ let nonce = &bytes[..NONCE_SIZE];
+ if nonce.len() != NONCE_SIZE {
+ return Err(CipherError::NoNonce);
+ }
+ let nonce = GenericArray::from_slice(nonce);
+ let ciphertext = &bytes[NONCE_SIZE..];
+
+ 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);
+ }
+}
+
+#[derive(Debug, thiserror::Error)]
+pub enum CipherError {
+ #[error("failed to encrypt with AES-GEM: {0}")]
+ EncryptError(aes_gcm::Error),
+
+ #[error("encrypted chunk does not start with correct version")]
+ UnknownChunkVersion,
+
+ #[error("encrypted chunk does not have a complete nonce")]
+ NoNonce,
+
+ #[error("failed to decrypt with AES-GEM: {0}")]
+ DecryptError(aes_gcm::Error),
+
+ #[error("failed to parse decrypted data as a DataChunk: {0}")]
+ Parse(serde_yaml::Error),
+
+ #[error(transparent)]
+ Utf8Error(#[from] std::str::Utf8Error),
+
+ #[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;
+ use crate::passwords::Passwords;
+
+ #[test]
+ fn metadata_as_aad() {
+ let meta = ChunkMeta::new("dummy-checksum");
+ 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 meta = ChunkMeta::new("dummy-checksum");
+ 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);
+ }
+}
diff --git a/src/error.rs b/src/error.rs
index 8241d5d..e4d77d3 100644
--- a/src/error.rs
+++ b/src/error.rs
@@ -1,4 +1,5 @@
use crate::backup_run::BackupError;
+use crate::cipher::CipherError;
use crate::client::ClientError;
use crate::cmd::restore::RestoreError;
use crate::config::ClientConfigError;
@@ -32,6 +33,9 @@ pub enum ObnamError {
NascentError(#[from] NascentError),
#[error(transparent)]
+ CipherError(#[from] CipherError),
+
+ #[error(transparent)]
LocalGenerationError(#[from] LocalGenerationError),
#[error(transparent)]
diff --git a/src/lib.rs b/src/lib.rs
index 82dab15..7d7afdc 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -7,6 +7,7 @@ pub mod chunk;
pub mod chunker;
pub mod chunkid;
pub mod chunkmeta;
+pub mod cipher;
pub mod client;
pub mod cmd;
pub mod config;