diff --git a/Cargo.lock b/Cargo.lock index 10f51edf9..9ac25caa0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2961,6 +2961,7 @@ name = "taskchampion" version = "0.4.1" dependencies = [ "anyhow", + "byteorder", "chrono", "flate2", "log", diff --git a/taskchampion/Cargo.toml b/taskchampion/Cargo.toml index c5da9898c..b48ed4491 100644 --- a/taskchampion/Cargo.toml +++ b/taskchampion/Cargo.toml @@ -24,6 +24,7 @@ rusqlite = { version = "0.25", features = ["bundled"] } strum = "0.21" strum_macros = "0.21" flate2 = "1" +byteorder = "1.0" [dev-dependencies] proptest = "^1.0.0" diff --git a/taskchampion/src/server/remote/crypto.rs b/taskchampion/src/server/remote/crypto.rs index 40103f422..65bdddbfa 100644 --- a/taskchampion/src/server/remote/crypto.rs +++ b/taskchampion/src/server/remote/crypto.rs @@ -1,5 +1,6 @@ use crate::server::HistorySegment; -use std::io::Read; +use byteorder::{NetworkEndian, ReadBytesExt, WriteBytesExt}; +use std::io::{Cursor, Read}; use tindercrypt::cryptors::RingCryptor; use uuid::Uuid; @@ -17,6 +18,60 @@ impl AsRef<[u8]> for Secret { } } +const PBKDF2_SALT_SIZE: usize = 32; +const NONCE_SIZE: usize = 12; +const ENVELOPE_VERSION: u32 = 1; + +/// Envelope for the data stored on the server, containing the information +/// required to decrypt. +#[derive(Debug, PartialEq, Eq)] +struct Envelope { + kdf_iterations: u32, + kdf_salt: [u8; PBKDF2_SALT_SIZE], + nonce: [u8; NONCE_SIZE], +} + +impl Envelope { + const SIZE: usize = 4 + 4 + PBKDF2_SALT_SIZE + NONCE_SIZE; + + fn from_bytes(buf: &[u8]) -> anyhow::Result { + if buf.len() < 4 { + anyhow::bail!("envelope is too small"); + } + + let mut rdr = Cursor::new(buf); + let version = rdr.read_u32::().unwrap(); + if version != 1 { + anyhow::bail!("unrecognized envelope version {}", version); + } + if buf.len() != Envelope::SIZE { + anyhow::bail!("envelope size {} is not {}", buf.len(), Envelope::SIZE); + } + + let kdf_iterations = rdr.read_u32::().unwrap(); + + let mut env = Envelope { + kdf_iterations, + kdf_salt: [0; PBKDF2_SALT_SIZE], + nonce: [0; NONCE_SIZE], + }; + env.kdf_salt.clone_from_slice(&buf[8..8 + PBKDF2_SALT_SIZE]); + env.nonce + .clone_from_slice(&buf[8 + PBKDF2_SALT_SIZE..8 + PBKDF2_SALT_SIZE + NONCE_SIZE]); + Ok(env) // TODO: test + } + + fn to_bytes(&self) -> Vec { + let mut buf = Vec::with_capacity(Envelope::SIZE); + + buf.write_u32::(ENVELOPE_VERSION).unwrap(); + buf.write_u32::(self.kdf_iterations).unwrap(); + buf.extend_from_slice(&self.kdf_salt); + buf.extend_from_slice(&self.nonce); + buf // TODO: test + } +} + /// A cleartext payload with an attached version_id. The version_id is used to /// validate the context of the payload. pub(super) struct Cleartext { @@ -75,6 +130,19 @@ mod test { use super::*; use pretty_assertions::assert_eq; + #[test] + fn envelope_round_trip() { + let env = Envelope { + kdf_iterations: 100, + kdf_salt: [1; 32], + nonce: [2; 12], + }; + + let bytes = env.to_bytes(); + let env2 = Envelope::from_bytes(&bytes).unwrap(); + assert_eq!(env, env2); + } + #[test] fn round_trip() { let version_id = Uuid::new_v4();