add an app_id to the encryption AAD

This commit is contained in:
Dustin J. Mitchell 2021-10-17 18:06:16 -04:00
parent 4300f7bdda
commit 17f5521ea4
2 changed files with 58 additions and 19 deletions

View file

@ -53,21 +53,29 @@ The salt is the SHA256 hash of the 16-byte form of the client key.
#### Encryption #### Encryption
The client uses [AEAD](https://commondatastorage.googleapis.com/chromium-boringssl-docs/aead.h.html), with algorithm CHACHA20_POLY1305. The client uses [AEAD](https://commondatastorage.googleapis.com/chromium-boringssl-docs/aead.h.html), with algorithm CHACHA20_POLY1305.
Each encrypted payload has an associated version ID.
The 16-byte form of this UUID is used as the associated data (AAD) with the AEAD algorithm.
The client should generate a random nonce, noting that AEAD is _not secure_ if a nonce is used repeatedly for the same key. The client should generate a random nonce, noting that AEAD is _not secure_ if a nonce is used repeatedly for the same key.
AEAD supports additional authenticated data (AAD) which must be provided for both open and seal operations.
In this protocol, the AAD is always 17 bytes of the form:
* `app_id` (byte) - always 1
* `version_id` (16 bytes) - 16-byte form of the version ID associated with this data
* for versions (AddVersion, GetChildVersion), the _parent_ version_id
* for snapshots (AddSnapshot, GetSnapshot), the snapshot version_id
The `app_id` field is for future expansion to handle other, non-task data using this protocol.
Including it in the AAD ensures that such data cannot be confused with task data.
Although the AEAD specification distinguishes ciphertext and tags, for purposes of this specification they are considered concatenated into a single bytestring as in BoringSSL's `EVP_AEAD_CTX_seal`. Although the AEAD specification distinguishes ciphertext and tags, for purposes of this specification they are considered concatenated into a single bytestring as in BoringSSL's `EVP_AEAD_CTX_seal`.
#### Representation #### Representation
The final byte-stream is comprised of the following structure, with integers represented in network-endian format. The final byte-stream is comprised of the following structure:
* `version` (32-bit int) - format version (always 1) * `version` (byte) - format version (always 1)
* `nonce` (12 bytes) - encryption nonce * `nonce` (12 bytes) - encryption nonce
* `ciphertext` (remaining bytes) - ciphertext from sealing operation * `ciphertext` (remaining bytes) - ciphertext from sealing operation
Future versions may have a completely different format. The `version` field identifies this data format, and future formats will have a value other than 1 in this position.
### Version ### Version

View file

@ -1,12 +1,13 @@
/// This module implements the encryption specified in the sync-protocol /// This module implements the encryption specified in the sync-protocol
/// document. /// document.
use byteorder::{NetworkEndian, ReadBytesExt, WriteBytesExt};
use ring::{aead, digest, pbkdf2, rand, rand::SecureRandom}; use ring::{aead, digest, pbkdf2, rand, rand::SecureRandom};
use std::io::{Cursor, Read}; use std::io::Read;
use uuid::Uuid; use uuid::Uuid;
const PBKDF2_ITERATIONS: u32 = 100000; const PBKDF2_ITERATIONS: u32 = 100000;
const ENVELOPE_VERSION: u32 = 1; const ENVELOPE_VERSION: u8 = 1;
const AAD_LEN: usize = 17;
const TASK_APP_ID: u8 = 1;
/// An Cryptor stores a secret and allows sealing and unsealing. It derives a key from the secret, /// An Cryptor stores a secret and allows sealing and unsealing. It derives a key from the secret,
/// which takes a nontrivial amount of time, so it should be created once and re-used for the given /// which takes a nontrivial amount of time, so it should be created once and re-used for the given
@ -55,7 +56,7 @@ impl Cryptor {
.map_err(|_| anyhow::anyhow!("error generating random nonce"))?; .map_err(|_| anyhow::anyhow!("error generating random nonce"))?;
let nonce = aead::Nonce::assume_unique_for_key(nonce_buf); let nonce = aead::Nonce::assume_unique_for_key(nonce_buf);
let aad = aead::Aad::from(version_id.as_bytes()); let aad = self.make_aad(version_id);
let tag = self let tag = self
.key .key
@ -86,7 +87,7 @@ impl Cryptor {
let mut nonce = [0u8; aead::NONCE_LEN]; let mut nonce = [0u8; aead::NONCE_LEN];
nonce.copy_from_slice(env.nonce); nonce.copy_from_slice(env.nonce);
let nonce = aead::Nonce::assume_unique_for_key(nonce); let nonce = aead::Nonce::assume_unique_for_key(nonce);
let aad = aead::Aad::from(version_id.as_bytes()); let aad = self.make_aad(version_id);
let mut payload = env.payload.to_vec(); let mut payload = env.payload.to_vec();
let plaintext = self let plaintext = self
@ -99,6 +100,13 @@ impl Cryptor {
payload: plaintext.to_vec(), payload: plaintext.to_vec(),
}) })
} }
fn make_aad(&self, version_id: Uuid) -> aead::Aad<[u8; AAD_LEN]> {
let mut aad = [0u8; AAD_LEN];
aad[0] = TASK_APP_ID;
aad[1..].copy_from_slice(version_id.as_bytes());
aead::Aad::from(aad)
}
} }
/// Secret represents a secret key as used for encryption and decryption. /// Secret represents a secret key as used for encryption and decryption.
@ -126,26 +134,25 @@ struct Envelope<'a> {
impl<'a> Envelope<'a> { impl<'a> Envelope<'a> {
fn from_bytes(buf: &'a [u8]) -> anyhow::Result<Envelope<'a>> { fn from_bytes(buf: &'a [u8]) -> anyhow::Result<Envelope<'a>> {
if buf.len() <= 4 + aead::NONCE_LEN { if buf.len() <= 1 + aead::NONCE_LEN {
anyhow::bail!("envelope is too small"); anyhow::bail!("envelope is too small");
} }
let mut rdr = Cursor::new(buf); let version = buf[0];
let version = rdr.read_u32::<NetworkEndian>().unwrap(); if version != ENVELOPE_VERSION {
if version != 1 { anyhow::bail!("unrecognized encryption envelope version {}", version);
anyhow::bail!("unrecognized envelope version {}", version);
} }
Ok(Envelope { Ok(Envelope {
nonce: &buf[4..4 + aead::NONCE_LEN], nonce: &buf[1..1 + aead::NONCE_LEN],
payload: &buf[4 + aead::NONCE_LEN..], payload: &buf[1 + aead::NONCE_LEN..],
}) })
} }
fn to_bytes(&self) -> Vec<u8> { fn to_bytes(&self) -> Vec<u8> {
let mut buf = Vec::with_capacity(4 + self.nonce.len() + self.payload.len()); let mut buf = Vec::with_capacity(1 + self.nonce.len() + self.payload.len());
buf.write_u32::<NetworkEndian>(ENVELOPE_VERSION).unwrap(); buf.push(ENVELOPE_VERSION);
buf.extend_from_slice(self.nonce); buf.extend_from_slice(self.nonce);
buf.extend_from_slice(self.payload); buf.extend_from_slice(self.payload);
buf buf
@ -210,6 +217,30 @@ mod test {
assert_eq!(env, env2); assert_eq!(env, env2);
} }
#[test]
fn envelope_bad_version() {
let env = Envelope {
nonce: &[2; 12],
payload: b"HELLO",
};
let mut bytes = env.to_bytes();
bytes[0] = 99;
assert!(Envelope::from_bytes(&bytes).is_err());
}
#[test]
fn envelope_too_short() {
let env = Envelope {
nonce: &[2; 12],
payload: b"HELLO",
};
let bytes = env.to_bytes();
let bytes = &bytes[..10];
assert!(Envelope::from_bytes(bytes).is_err());
}
#[test] #[test]
fn round_trip() { fn round_trip() {
let version_id = Uuid::new_v4(); let version_id = Uuid::new_v4();