Merge pull request #301 from djmitche/issue299

Drop tindercrypt, document encryption
This commit is contained in:
Dustin J. Mitchell 2021-10-26 22:05:59 -04:00 committed by GitHub
commit 7c8c85f27f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 558 additions and 165 deletions

View file

@ -0,0 +1 @@
- The encryption format used for synchronization has changed incompatibly

21
Cargo.lock generated
View file

@ -2253,12 +2253,6 @@ dependencies = [
"tempfile", "tempfile",
] ]
[[package]]
name = "protobuf"
version = "2.24.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db50e77ae196458ccd3dc58a31ea1a90b0698ab1b7928d89f644c25d72070267"
[[package]] [[package]]
name = "pulldown-cmark" name = "pulldown-cmark"
version = "0.7.2" version = "0.7.2"
@ -2963,11 +2957,13 @@ name = "taskchampion"
version = "0.4.1" version = "0.4.1"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"byteorder",
"chrono", "chrono",
"flate2", "flate2",
"log", "log",
"pretty_assertions", "pretty_assertions",
"proptest", "proptest",
"ring",
"rstest", "rstest",
"rusqlite", "rusqlite",
"serde", "serde",
@ -2976,7 +2972,6 @@ dependencies = [
"strum_macros 0.21.1", "strum_macros 0.21.1",
"tempfile", "tempfile",
"thiserror", "thiserror",
"tindercrypt",
"ureq", "ureq",
"uuid", "uuid",
] ]
@ -3185,18 +3180,6 @@ dependencies = [
"syn", "syn",
] ]
[[package]]
name = "tindercrypt"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f34fd5cc8db265f27abf29e8a3cec5cc643ae9f3f9ae39f08a77dc4add375b6d"
dependencies = [
"protobuf",
"rand 0.7.3",
"ring",
"thiserror",
]
[[package]] [[package]]
name = "tinyvec" name = "tinyvec"
version = "1.2.0" version = "1.2.0"

View file

@ -42,7 +42,40 @@ The fourth invariant prevents the server from discarding versions newer than the
### Encryption ### Encryption
TBD (#299) The client configuration includes an encryption secret of arbitrary length and a clientId to identify itself.
This section describes how that information is used to encrypt and decrypt data sent to the server (versions and snapshots).
#### Key Derivation
The client derives the 32-byte encryption key from the configured encryption secret using PBKDF2 with HMAC-SHA256 and 100,000 iterations.
The salt is the SHA256 hash of the 16-byte form of the client key.
#### Encryption
The client uses [AEAD](https://commondatastorage.googleapis.com/chromium-boringssl-docs/aead.h.html), with algorithm CHACHA20_POLY1305.
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`.
#### Representation
The final byte-stream is comprised of the following structure:
* `version` (byte) - format version (always 1)
* `nonce` (12 bytes) - encryption nonce
* `ciphertext` (remaining bytes) - ciphertext from sealing operation
The `version` field identifies this data format, and future formats will have a value other than 1 in this position.
### Version ### Version

View file

@ -19,11 +19,12 @@ anyhow = "1.0"
thiserror = "1.0" thiserror = "1.0"
ureq = "^2.1.0" ureq = "^2.1.0"
log = "^0.4.14" log = "^0.4.14"
tindercrypt = { version = "^0.2.2", default-features = false }
rusqlite = { version = "0.25", features = ["bundled"] } rusqlite = { version = "0.25", features = ["bundled"] }
strum = "0.21" strum = "0.21"
strum_macros = "0.21" strum_macros = "0.21"
flate2 = "1" flate2 = "1"
byteorder = "1.0"
ring = "0.16"
[dev-dependencies] [dev-dependencies]
proptest = "^1.0.0" proptest = "^1.0.0"

View file

@ -33,7 +33,7 @@ impl ServerConfig {
origin, origin,
client_key, client_key,
encryption_secret, encryption_secret,
} => Box::new(RemoteServer::new(origin, client_key, encryption_secret)), } => Box::new(RemoteServer::new(origin, client_key, encryption_secret)?),
}) })
} }
} }

View file

@ -0,0 +1,412 @@
/// This module implements the encryption specified in the sync-protocol
/// document.
use ring::{aead, digest, pbkdf2, rand, rand::SecureRandom};
use std::io::Read;
use uuid::Uuid;
const PBKDF2_ITERATIONS: u32 = 100000;
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,
/// which takes a nontrivial amount of time, so it should be created once and re-used for the given
/// client_key.
pub(super) struct Cryptor {
key: aead::LessSafeKey,
rng: rand::SystemRandom,
}
impl Cryptor {
pub(super) fn new(client_key: Uuid, secret: &Secret) -> anyhow::Result<Self> {
Ok(Cryptor {
key: Self::derive_key(client_key, secret)?,
rng: rand::SystemRandom::new(),
})
}
/// Derive a key as specified for version 1. Note that this may take 10s of ms.
fn derive_key(client_key: Uuid, secret: &Secret) -> anyhow::Result<aead::LessSafeKey> {
let salt = digest::digest(&digest::SHA256, client_key.as_bytes());
let mut key_bytes = vec![0u8; aead::CHACHA20_POLY1305.key_len()];
pbkdf2::derive(
pbkdf2::PBKDF2_HMAC_SHA256,
std::num::NonZeroU32::new(PBKDF2_ITERATIONS).unwrap(),
salt.as_ref(),
secret.as_ref(),
&mut key_bytes,
);
let unbound_key = aead::UnboundKey::new(&aead::CHACHA20_POLY1305, &key_bytes)
.map_err(|_| anyhow::anyhow!("error while creating AEAD key"))?;
Ok(aead::LessSafeKey::new(unbound_key))
}
/// Encrypt the given payload.
pub(super) fn seal(&self, payload: Unsealed) -> anyhow::Result<Sealed> {
let Unsealed {
version_id,
mut payload,
} = payload;
let mut nonce_buf = [0u8; aead::NONCE_LEN];
self.rng
.fill(&mut nonce_buf)
.map_err(|_| anyhow::anyhow!("error generating random nonce"))?;
let nonce = aead::Nonce::assume_unique_for_key(nonce_buf);
let aad = self.make_aad(version_id);
let tag = self
.key
.seal_in_place_separate_tag(nonce, aad, &mut payload)
.map_err(|_| anyhow::anyhow!("error while sealing"))?;
payload.extend_from_slice(tag.as_ref());
let env = Envelope {
nonce: &nonce_buf,
payload: payload.as_ref(),
};
Ok(Sealed {
version_id,
payload: env.to_bytes(),
})
}
/// Decrypt the given payload, verifying it was created for the given version_id
pub(super) fn unseal(&self, payload: Sealed) -> anyhow::Result<Unsealed> {
let Sealed {
version_id,
payload,
} = payload;
let env = Envelope::from_bytes(&payload)?;
let mut nonce = [0u8; aead::NONCE_LEN];
nonce.copy_from_slice(env.nonce);
let nonce = aead::Nonce::assume_unique_for_key(nonce);
let aad = self.make_aad(version_id);
let mut payload = env.payload.to_vec();
let plaintext = self
.key
.open_in_place(nonce, aad, payload.as_mut())
.map_err(|_| anyhow::anyhow!("error while creating AEAD key"))?;
Ok(Unsealed {
version_id,
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.
pub(super) struct Secret(pub(super) Vec<u8>);
impl From<Vec<u8>> for Secret {
fn from(bytes: Vec<u8>) -> Self {
Self(bytes)
}
}
impl AsRef<[u8]> for Secret {
fn as_ref(&self) -> &[u8] {
&self.0
}
}
/// Envelope for the data stored on the server, containing the information
/// required to decrypt.
#[derive(Debug, PartialEq, Eq)]
struct Envelope<'a> {
nonce: &'a [u8],
payload: &'a [u8],
}
impl<'a> Envelope<'a> {
fn from_bytes(buf: &'a [u8]) -> anyhow::Result<Envelope<'a>> {
if buf.len() <= 1 + aead::NONCE_LEN {
anyhow::bail!("envelope is too small");
}
let version = buf[0];
if version != ENVELOPE_VERSION {
anyhow::bail!("unrecognized encryption envelope version {}", version);
}
Ok(Envelope {
nonce: &buf[1..1 + aead::NONCE_LEN],
payload: &buf[1 + aead::NONCE_LEN..],
})
}
fn to_bytes(&self) -> Vec<u8> {
let mut buf = Vec::with_capacity(1 + self.nonce.len() + self.payload.len());
buf.push(ENVELOPE_VERSION);
buf.extend_from_slice(self.nonce);
buf.extend_from_slice(self.payload);
buf
}
}
/// A unsealed payload with an attached version_id. The version_id is used to
/// validate the context of the payload on unsealing.
pub(super) struct Unsealed {
pub(super) version_id: Uuid,
pub(super) payload: Vec<u8>,
}
/// An encrypted payload
pub(super) struct Sealed {
pub(super) version_id: Uuid,
pub(super) payload: Vec<u8>,
}
impl Sealed {
pub(super) fn from_resp(
resp: ureq::Response,
version_id: Uuid,
content_type: &str,
) -> Result<Sealed, anyhow::Error> {
if resp.header("Content-Type") == Some(content_type) {
let mut reader = resp.into_reader();
let mut payload = vec![];
reader.read_to_end(&mut payload)?;
Ok(Self {
version_id,
payload,
})
} else {
Err(anyhow::anyhow!(
"Response did not have expected content-type"
))
}
}
}
impl AsRef<[u8]> for Sealed {
fn as_ref(&self) -> &[u8] {
self.payload.as_ref()
}
}
#[cfg(test)]
mod test {
use super::*;
use pretty_assertions::assert_eq;
#[test]
fn envelope_round_trip() {
let env = Envelope {
nonce: &[2; 12],
payload: b"HELLO",
};
let bytes = env.to_bytes();
let env2 = Envelope::from_bytes(&bytes).unwrap();
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]
fn round_trip() {
let version_id = Uuid::new_v4();
let payload = b"HISTORY REPEATS ITSELF".to_vec();
let secret = Secret(b"SEKRIT".to_vec());
let cryptor = Cryptor::new(Uuid::new_v4(), &secret).unwrap();
let unsealed = Unsealed {
version_id,
payload: payload.clone(),
};
let sealed = cryptor.seal(unsealed).unwrap();
let unsealed = cryptor.unseal(sealed).unwrap();
assert_eq!(unsealed.payload, payload);
assert_eq!(unsealed.version_id, version_id);
}
#[test]
fn round_trip_bad_key() {
let version_id = Uuid::new_v4();
let payload = b"HISTORY REPEATS ITSELF".to_vec();
let client_key = Uuid::new_v4();
let secret = Secret(b"SEKRIT".to_vec());
let cryptor = Cryptor::new(client_key, &secret).unwrap();
let unsealed = Unsealed {
version_id,
payload: payload.clone(),
};
let sealed = cryptor.seal(unsealed).unwrap();
let secret = Secret(b"DIFFERENT_SECRET".to_vec());
let cryptor = Cryptor::new(client_key, &secret).unwrap();
assert!(cryptor.unseal(sealed).is_err());
}
#[test]
fn round_trip_bad_version() {
let version_id = Uuid::new_v4();
let payload = b"HISTORY REPEATS ITSELF".to_vec();
let client_key = Uuid::new_v4();
let secret = Secret(b"SEKRIT".to_vec());
let cryptor = Cryptor::new(client_key, &secret).unwrap();
let unsealed = Unsealed {
version_id,
payload: payload.clone(),
};
let mut sealed = cryptor.seal(unsealed).unwrap();
sealed.version_id = Uuid::new_v4(); // change the version_id
assert!(cryptor.unseal(sealed).is_err());
}
#[test]
fn round_trip_bad_client_key() {
let version_id = Uuid::new_v4();
let payload = b"HISTORY REPEATS ITSELF".to_vec();
let client_key = Uuid::new_v4();
let secret = Secret(b"SEKRIT".to_vec());
let cryptor = Cryptor::new(client_key, &secret).unwrap();
let unsealed = Unsealed {
version_id,
payload: payload.clone(),
};
let sealed = cryptor.seal(unsealed).unwrap();
let client_key = Uuid::new_v4();
let cryptor = Cryptor::new(client_key, &secret).unwrap();
assert!(cryptor.unseal(sealed).is_err());
}
mod externally_valid {
// validate data generated by generate-test-data.py. The intent is to
// validate that this format matches the specification by implementing
// the specification in a second language
use super::*;
use pretty_assertions::assert_eq;
/// The values in generate-test-data.py
fn defaults() -> (Uuid, Uuid, Vec<u8>) {
(
Uuid::parse_str("b0517957-f912-4d49-8330-f612e73030c4").unwrap(),
Uuid::parse_str("0666d464-418a-4a08-ad53-6f15c78270cd").unwrap(),
b"b4a4e6b7b811eda1dc1a2693ded".to_vec(),
)
}
#[test]
fn good() {
let (version_id, client_key, encryption_secret) = defaults();
let sealed = Sealed {
version_id,
payload: include_bytes!("test-good.data").to_vec(),
};
let cryptor = Cryptor::new(client_key, &Secret(encryption_secret)).unwrap();
let unsealed = cryptor.unseal(sealed).unwrap();
assert_eq!(unsealed.payload, b"SUCCESS");
assert_eq!(unsealed.version_id, version_id);
}
#[test]
fn bad_version_id() {
let (version_id, client_key, encryption_secret) = defaults();
let sealed = Sealed {
version_id,
payload: include_bytes!("test-bad-version-id.data").to_vec(),
};
let cryptor = Cryptor::new(client_key, &Secret(encryption_secret)).unwrap();
assert!(cryptor.unseal(sealed).is_err());
}
#[test]
fn bad_client_key() {
let (version_id, client_key, encryption_secret) = defaults();
let sealed = Sealed {
version_id,
payload: include_bytes!("test-bad-client-key.data").to_vec(),
};
let cryptor = Cryptor::new(client_key, &Secret(encryption_secret)).unwrap();
assert!(cryptor.unseal(sealed).is_err());
}
#[test]
fn bad_secret() {
let (version_id, client_key, encryption_secret) = defaults();
let sealed = Sealed {
version_id,
payload: include_bytes!("test-bad-secret.data").to_vec(),
};
let cryptor = Cryptor::new(client_key, &Secret(encryption_secret)).unwrap();
assert!(cryptor.unseal(sealed).is_err());
}
#[test]
fn bad_version() {
let (version_id, client_key, encryption_secret) = defaults();
let sealed = Sealed {
version_id,
payload: include_bytes!("test-bad-version.data").to_vec(),
};
let cryptor = Cryptor::new(client_key, &Secret(encryption_secret)).unwrap();
assert!(cryptor.unseal(sealed).is_err());
}
#[test]
fn bad_app_id() {
let (version_id, client_key, encryption_secret) = defaults();
let sealed = Sealed {
version_id,
payload: include_bytes!("test-bad-app-id.data").to_vec(),
};
let cryptor = Cryptor::new(client_key, &Secret(encryption_secret)).unwrap();
assert!(cryptor.unseal(sealed).is_err());
}
}
}

View file

@ -0,0 +1,77 @@
# This file generates test-encrypted.data. To run it:
# - pip install cryptography pbkdf2
# - python taskchampion/src/server/generate-test-data.py taskchampion/src/server/
import os
import hashlib
import pbkdf2
import secrets
import sys
import uuid
from cryptography.hazmat.primitives.ciphers.aead import ChaCha20Poly1305
# these values match values used in the rust tests
client_key = "0666d464-418a-4a08-ad53-6f15c78270cd"
encryption_secret = b"b4a4e6b7b811eda1dc1a2693ded"
version_id = "b0517957-f912-4d49-8330-f612e73030c4"
def gen(
version_id=version_id, client_key=client_key, encryption_secret=encryption_secret,
app_id=1, version=1):
# first, generate the encryption key
salt = hashlib.sha256(uuid.UUID(client_key).bytes).digest()
key = pbkdf2.PBKDF2(
encryption_secret,
salt,
digestmodule=hashlib.sha256,
iterations=100000,
).read(32)
# create a nonce
nonce = secrets.token_bytes(12)
assert len(b"\x01") == 1
# create the AAD
aad = b''.join([
bytes([app_id]),
uuid.UUID(version_id).bytes,
])
# encrypt using AEAD
chacha = ChaCha20Poly1305(key)
ciphertext = chacha.encrypt(nonce, b"SUCCESS", aad)
# create the envelope
envelope = b''.join([
bytes([version]),
nonce,
ciphertext,
])
return envelope
def main():
dir = sys.argv[1]
with open(os.path.join(dir, 'test-good.data'), "wb") as f:
f.write(gen())
with open(os.path.join(dir, 'test-bad-version-id.data'), "wb") as f:
f.write(gen(version_id=uuid.uuid4().hex))
with open(os.path.join(dir, 'test-bad-client-key.data'), "wb") as f:
f.write(gen(client_key=uuid.uuid4().hex))
with open(os.path.join(dir, 'test-bad-secret.data'), "wb") as f:
f.write(gen(encryption_secret=b"xxxxxxxxxxxxxxxxxxxxx"))
with open(os.path.join(dir, 'test-bad-version.data'), "wb") as f:
f.write(gen(version=99))
with open(os.path.join(dir, 'test-bad-app-id.data'), "wb") as f:
f.write(gen(app_id=99))
main()

View file

@ -12,6 +12,7 @@ However, users who wish to implement their own server interfaces can implement t
pub(crate) mod test; pub(crate) mod test;
mod config; mod config;
mod crypto;
mod local; mod local;
mod remote; mod remote;
mod types; mod types;

View file

@ -1,126 +0,0 @@
use crate::server::HistorySegment;
use std::io::Read;
use tindercrypt::cryptors::RingCryptor;
use uuid::Uuid;
pub(super) struct Secret(pub(super) Vec<u8>);
impl From<Vec<u8>> for Secret {
fn from(bytes: Vec<u8>) -> Self {
Self(bytes)
}
}
impl AsRef<[u8]> for Secret {
fn as_ref(&self) -> &[u8] {
&self.0
}
}
/// A cleartext payload with an attached version_id. The version_id is used to
/// validate the context of the payload.
pub(super) struct Cleartext {
pub(super) version_id: Uuid,
pub(super) payload: HistorySegment,
}
impl Cleartext {
/// Seal the payload into its ciphertext
pub(super) fn seal(self, secret: &Secret) -> anyhow::Result<Ciphertext> {
let cryptor = RingCryptor::new().with_aad(self.version_id.as_bytes());
let ciphertext = cryptor.seal_with_passphrase(secret.as_ref(), &self.payload)?;
Ok(Ciphertext(ciphertext))
}
}
/// An ecrypted payload
pub(super) struct Ciphertext(pub(super) Vec<u8>);
impl Ciphertext {
pub(super) fn from_resp(
resp: ureq::Response,
content_type: &str,
) -> Result<Ciphertext, anyhow::Error> {
if resp.header("Content-Type") == Some(content_type) {
let mut reader = resp.into_reader();
let mut bytes = vec![];
reader.read_to_end(&mut bytes)?;
Ok(Self(bytes))
} else {
Err(anyhow::anyhow!(
"Response did not have expected content-type"
))
}
}
pub(super) fn open(self, secret: &Secret, version_id: Uuid) -> anyhow::Result<Cleartext> {
let cryptor = RingCryptor::new().with_aad(version_id.as_bytes());
let plaintext = cryptor.open(secret.as_ref(), &self.0)?;
Ok(Cleartext {
version_id,
payload: plaintext,
})
}
}
impl AsRef<[u8]> for Ciphertext {
fn as_ref(&self) -> &[u8] {
self.0.as_ref()
}
}
#[cfg(test)]
mod test {
use super::*;
use pretty_assertions::assert_eq;
#[test]
fn round_trip() {
let version_id = Uuid::new_v4();
let payload = b"HISTORY REPEATS ITSELF".to_vec();
let secret = Secret(b"SEKRIT".to_vec());
let cleartext = Cleartext {
version_id,
payload: payload.clone(),
};
let ciphertext = cleartext.seal(&secret).unwrap();
let cleartext = ciphertext.open(&secret, version_id).unwrap();
assert_eq!(cleartext.payload, payload);
assert_eq!(cleartext.version_id, version_id);
}
#[test]
fn round_trip_bad_key() {
let version_id = Uuid::new_v4();
let payload = b"HISTORY REPEATS ITSELF".to_vec();
let secret = Secret(b"SEKRIT".to_vec());
let cleartext = Cleartext {
version_id,
payload: payload.clone(),
};
let ciphertext = cleartext.seal(&secret).unwrap();
let secret = Secret(b"BADSEKRIT".to_vec());
assert!(ciphertext.open(&secret, version_id).is_err());
}
#[test]
fn round_trip_bad_version() {
let version_id = Uuid::new_v4();
let payload = b"HISTORY REPEATS ITSELF".to_vec();
let secret = Secret(b"SEKRIT".to_vec());
let cleartext = Cleartext {
version_id,
payload: payload.clone(),
};
let ciphertext = cleartext.seal(&secret).unwrap();
let bad_version_id = Uuid::new_v4();
assert!(ciphertext.open(&secret, bad_version_id).is_err());
}
}

View file

@ -5,13 +5,12 @@ use crate::server::{
use std::time::Duration; use std::time::Duration;
use uuid::Uuid; use uuid::Uuid;
mod crypto; use super::crypto::{Cryptor, Sealed, Secret, Unsealed};
use crypto::{Ciphertext, Cleartext, Secret};
pub struct RemoteServer { pub struct RemoteServer {
origin: String, origin: String,
client_key: Uuid, client_key: Uuid,
encryption_secret: Secret, cryptor: Cryptor,
agent: ureq::Agent, agent: ureq::Agent,
} }
@ -28,16 +27,20 @@ impl RemoteServer {
/// without a trailing slash, such as `https://tcsync.example.com`. Pass a client_key to /// without a trailing slash, such as `https://tcsync.example.com`. Pass a client_key to
/// identify this client to the server. Multiple replicas synchronizing the same task history /// identify this client to the server. Multiple replicas synchronizing the same task history
/// should use the same client_key. /// should use the same client_key.
pub fn new(origin: String, client_key: Uuid, encryption_secret: Vec<u8>) -> RemoteServer { pub fn new(
RemoteServer { origin: String,
client_key: Uuid,
encryption_secret: Vec<u8>,
) -> anyhow::Result<RemoteServer> {
Ok(RemoteServer {
origin, origin,
client_key, client_key,
encryption_secret: encryption_secret.into(), cryptor: Cryptor::new(client_key, &Secret(encryption_secret.to_vec()))?,
agent: ureq::AgentBuilder::new() agent: ureq::AgentBuilder::new()
.timeout_connect(Duration::from_secs(10)) .timeout_connect(Duration::from_secs(10))
.timeout_read(Duration::from_secs(60)) .timeout_read(Duration::from_secs(60))
.build(), .build(),
} })
} }
} }
@ -73,17 +76,17 @@ impl Server for RemoteServer {
"{}/v1/client/add-version/{}", "{}/v1/client/add-version/{}",
self.origin, parent_version_id self.origin, parent_version_id
); );
let cleartext = Cleartext { let unsealed = Unsealed {
version_id: parent_version_id, version_id: parent_version_id,
payload: history_segment, payload: history_segment,
}; };
let ciphertext = cleartext.seal(&self.encryption_secret)?; let sealed = self.cryptor.seal(unsealed)?;
match self match self
.agent .agent
.post(&url) .post(&url)
.set("Content-Type", HISTORY_SEGMENT_CONTENT_TYPE) .set("Content-Type", HISTORY_SEGMENT_CONTENT_TYPE)
.set("X-Client-Key", &self.client_key.to_string()) .set("X-Client-Key", &self.client_key.to_string())
.send_bytes(ciphertext.as_ref()) .send_bytes(sealed.as_ref())
{ {
Ok(resp) => { Ok(resp) => {
let version_id = get_uuid_header(&resp, "X-Version-Id")?; let version_id = get_uuid_header(&resp, "X-Version-Id")?;
@ -120,10 +123,9 @@ impl Server for RemoteServer {
Ok(resp) => { Ok(resp) => {
let parent_version_id = get_uuid_header(&resp, "X-Parent-Version-Id")?; let parent_version_id = get_uuid_header(&resp, "X-Parent-Version-Id")?;
let version_id = get_uuid_header(&resp, "X-Version-Id")?; let version_id = get_uuid_header(&resp, "X-Version-Id")?;
let ciphertext = Ciphertext::from_resp(resp, HISTORY_SEGMENT_CONTENT_TYPE)?; let sealed =
let history_segment = ciphertext Sealed::from_resp(resp, parent_version_id, HISTORY_SEGMENT_CONTENT_TYPE)?;
.open(&self.encryption_secret, parent_version_id)? let history_segment = self.cryptor.unseal(sealed)?.payload;
.payload;
Ok(GetVersionResult::Version { Ok(GetVersionResult::Version {
version_id, version_id,
parent_version_id, parent_version_id,
@ -139,17 +141,17 @@ impl Server for RemoteServer {
fn add_snapshot(&mut self, version_id: VersionId, snapshot: Snapshot) -> anyhow::Result<()> { fn add_snapshot(&mut self, version_id: VersionId, snapshot: Snapshot) -> anyhow::Result<()> {
let url = format!("{}/v1/client/add-snapshot/{}", self.origin, version_id); let url = format!("{}/v1/client/add-snapshot/{}", self.origin, version_id);
let cleartext = Cleartext { let unsealed = Unsealed {
version_id, version_id,
payload: snapshot, payload: snapshot,
}; };
let ciphertext = cleartext.seal(&self.encryption_secret)?; let sealed = self.cryptor.seal(unsealed)?;
Ok(self Ok(self
.agent .agent
.post(&url) .post(&url)
.set("Content-Type", SNAPSHOT_CONTENT_TYPE) .set("Content-Type", SNAPSHOT_CONTENT_TYPE)
.set("X-Client-Key", &self.client_key.to_string()) .set("X-Client-Key", &self.client_key.to_string())
.send_bytes(ciphertext.as_ref()) .send_bytes(sealed.as_ref())
.map(|_| ())?) .map(|_| ())?)
} }

View file

@ -0,0 +1,2 @@
$á­
†—Õ^~B>n)ji†¯1—î9™|µœÓ~

View file

@ -0,0 +1 @@
<01>ΝA4φ―Γθ t;Δτ υηp¦Ο¦x^Αύreό…<CF8C>JΤ¤<CEA4>

View file

@ -0,0 +1 @@
/}åd E°‡dIcÁXéè-‡!V°Û%è4îáòd]³ÃÇ}

View file

@ -0,0 +1 @@
lΰζδa|ο@Ο<>S_¬…γzέV9£q¦Ρ…‘)+¦

View file

@ -0,0 +1 @@
c╙╤TH╗Гp>╔╚Ф╨╕m4О╧к~в1╣0P░IЖ╢W╒

View file

@ -0,0 +1,2 @@
B∙
Ат-в3%╕jё,*ъ╨7Й╘√QьKЗO╕°FPZщ

View file

@ -0,0 +1 @@
pÑ¿µÒŸ½V²ûÝäToë"}cT·äY7Æ ˆÀ@ÙdLTý`Ò