use thiserror::Error;
pub const MAGIC: &[u8; 4] = b"NDBB";
pub const VERSION: u8 = 3;
pub const HEADER_LEN: usize = 52;
pub const SECTION_OVERHEAD: usize = 16;
pub const TRAILER_LEN: usize = 4;
pub const DEFAULT_MAX_TOTAL_BYTES: u64 = 16 * 1024 * 1024 * 1024;
pub const DEFAULT_MAX_SECTION_BYTES: u64 = 16 * 1024 * 1024 * 1024;
pub const SECTION_ORIGIN_CATALOG_ROWS: u64 = 0xFFFF_FFFF_FFFF_FFF0;
pub const SECTION_ORIGIN_SOURCE_TOMBSTONES: u64 = 0xFFFF_FFFF_FFFF_FFF1;
pub const SECTION_ORIGIN_SURROGATE_PK: u64 = 0xFFFF_FFFF_FFFF_FFF2;
pub const SECTION_ORIGIN_DATABASES: u64 = 0xFFFF_FFFF_FFFF_FFF3;
pub const SECTION_ORIGIN_VERIFICATION: u64 = 0xFFFF_FFFF_FFFF_FFF4;
pub const SECTION_ORIGIN_ARRAY_CATALOG: u64 = 0xFFFF_FFFF_FFFF_FFF5;
#[derive(Debug, Clone, PartialEq, Eq, zerompk::ToMessagePack, zerompk::FromMessagePack)]
pub struct DatabaseBlob {
pub database_id: u64,
pub name: String,
pub descriptor: Vec<u8>,
pub database_quota: Option<crate::QuotaRecord>,
pub tenant_quota: Option<crate::QuotaRecord>,
}
#[derive(Debug, Clone, PartialEq, Eq, zerompk::ToMessagePack, zerompk::FromMessagePack)]
pub struct DatabaseDataSection {
pub database_id: u64,
pub snapshot: Vec<u8>,
}
#[derive(Debug, Clone, PartialEq, Eq, zerompk::ToMessagePack, zerompk::FromMessagePack)]
pub struct StoredCollectionBlob {
pub database_id: u64,
pub name: String,
pub bytes: Vec<u8>,
}
#[derive(Debug, Clone, PartialEq, Eq, zerompk::ToMessagePack, zerompk::FromMessagePack)]
pub struct ArrayCatalogBlob {
pub database_id: u64,
pub name: String,
pub bytes: Vec<u8>,
}
#[derive(Debug, Clone, PartialEq, Eq, zerompk::ToMessagePack, zerompk::FromMessagePack)]
pub struct SourceTombstoneEntry {
pub database_id: u64,
pub collection: String,
pub purge_lsn: u64,
}
#[derive(Debug, Clone, PartialEq, Eq, zerompk::ToMessagePack, zerompk::FromMessagePack)]
pub struct SurrogateBindBlob {
pub database_id: u64,
pub tenant_id: u64,
pub collection: String,
pub pk: Vec<u8>,
pub surrogate: u32,
}
#[derive(Debug, Error, PartialEq, Eq)]
#[non_exhaustive]
pub enum EnvelopeError {
#[error("invalid backup format")]
BadMagic,
#[error("unsupported backup version: {0}")]
UnsupportedVersion(u8),
#[error("invalid backup format")]
HeaderCrcMismatch,
#[error("invalid backup format")]
BodyCrcMismatch,
#[error("invalid backup format")]
TrailerCrcMismatch,
#[error("backup truncated")]
Truncated,
#[error("backup tenant mismatch: expected {expected}, got {actual}")]
TenantMismatch { expected: u64, actual: u64 },
#[error("backup exceeds size cap of {cap} bytes")]
OverSizeTotal { cap: u64 },
#[error("backup section exceeds size cap of {cap} bytes")]
OverSizeSection { cap: u64 },
#[error("too many sections: {0}")]
TooManySections(u16),
#[error("wrong backup KEK: presented key fingerprint does not match envelope")]
WrongBackupKek,
#[error("backup decryption failed: authentication tag mismatch")]
DecryptionFailed,
#[error("backup encryption failed")]
EncryptionFailed,
#[error("backup encryption failed: {0}")]
RandomFailure(String),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct EnvelopeMeta {
pub tenant_id: u64,
pub source_vshard_count: u16,
pub hash_seed: u64,
pub snapshot_watermark: u64,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Section {
pub origin_node_id: u64,
pub body: Vec<u8>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Envelope {
pub meta: EnvelopeMeta,
pub sections: Vec<Section>,
}
pub fn read2(s: &[u8]) -> [u8; 2] {
[s[0], s[1]]
}
pub fn read4(s: &[u8]) -> [u8; 4] {
[s[0], s[1], s[2], s[3]]
}
pub fn read8(s: &[u8]) -> [u8; 8] {
[s[0], s[1], s[2], s[3], s[4], s[5], s[6], s[7]]
}
#[cfg(test)]
mod tests {
use super::*;
use crate::backup_envelope::{parse_encrypted, write::EnvelopeWriter};
const KEK: [u8; 32] = [0x5Au8; 32];
fn meta() -> EnvelopeMeta {
EnvelopeMeta {
tenant_id: 3,
source_vshard_count: 1024,
hash_seed: 0,
snapshot_watermark: 17,
}
}
#[test]
fn database_sections_round_trip_through_an_encrypted_envelope() {
let databases = vec![
DatabaseBlob {
database_id: 0,
name: "default".into(),
descriptor: vec![1],
database_quota: None,
tenant_quota: None,
},
DatabaseBlob {
database_id: 1025,
name: "sales".into(),
descriptor: vec![2],
database_quota: Some(crate::QuotaRecord::DEFAULT),
tenant_quota: None,
},
];
let data = DatabaseDataSection {
database_id: 1025,
snapshot: vec![9, 8, 7],
};
let mut writer = EnvelopeWriter::new(meta());
writer
.push_section(
SECTION_ORIGIN_DATABASES,
zerompk::to_msgpack_vec(&databases).expect("encode databases"),
)
.expect("push databases");
writer
.push_section(7, zerompk::to_msgpack_vec(&data).expect("encode data"))
.expect("push data");
let bytes = writer.finalize_encrypted(&KEK).expect("encrypt");
let env = parse_encrypted(&bytes, DEFAULT_MAX_TOTAL_BYTES, &KEK).expect("parse");
let decoded: Vec<DatabaseBlob> =
zerompk::from_msgpack(&env.sections[0].body).expect("decode databases");
assert_eq!(decoded, databases);
let decoded: DatabaseDataSection =
zerompk::from_msgpack(&env.sections[1].body).expect("decode data");
assert_eq!(decoded, data);
}
#[test]
fn a_tampered_database_section_fails_authentication() {
let data = DatabaseDataSection {
database_id: 1025,
snapshot: vec![1, 2, 3, 4],
};
let mut writer = EnvelopeWriter::new(meta());
writer
.push_section(7, zerompk::to_msgpack_vec(&data).expect("encode data"))
.expect("push data");
let mut bytes = writer.finalize_encrypted(&KEK).expect("encrypt");
let body_start = HEADER_LEN + 68 + 8 + 4 + 12;
bytes[body_start] ^= 0xFF;
let body_len = u32::from_le_bytes(read4(&bytes[HEADER_LEN + 68 + 8..])) as usize;
let body_end = body_start + body_len;
let body_crc = crc32c::crc32c(&bytes[body_start..body_end]);
bytes[body_end..body_end + 4].copy_from_slice(&body_crc.to_le_bytes());
let trailer_start = bytes.len() - TRAILER_LEN;
let trailer_crc = crc32c::crc32c(&bytes[..trailer_start]);
bytes[trailer_start..].copy_from_slice(&trailer_crc.to_le_bytes());
assert_eq!(
parse_encrypted(&bytes, DEFAULT_MAX_TOTAL_BYTES, &KEK),
Err(EnvelopeError::DecryptionFailed)
);
}
}