From 401a0fd37f523d974ca4ca190c97579cbf943c5a Mon Sep 17 00:00:00 2001 From: "Franz Heinzmann (Frando)" Date: Sat, 13 Jun 2020 17:25:09 +0200 Subject: [PATCH] Encrypt HTTP uploads with PGP (symmetrically) The symmetric key (passphrase) is put into the fragment part of the upload URL. The upload URL is part of the message as a protected header and in the body payload. If the message is encrypted itself, the symmetric key is encrypted with the message. --- src/pgp.rs | 22 ++++++++++++---- src/upload.rs | 71 ++++++++++++++++++++++++++++++++++++++++++++++----- 2 files changed, 81 insertions(+), 12 deletions(-) diff --git a/src/pgp.rs b/src/pgp.rs index ee356b296..486736c89 100644 --- a/src/pgp.rs +++ b/src/pgp.rs @@ -10,6 +10,7 @@ use pgp::composed::{ SignedPublicSubKey, SignedSecretKey, SubkeyParamsBuilder, }; use pgp::crypto::{HashAlgorithm, SymmetricKeyAlgorithm}; +use pgp::ser::Serialize; use pgp::types::{ CompressionAlgorithm, KeyTrait, Mpi, PublicKeyTrait, SecretKeyTrait, StringToKey, }; @@ -322,8 +323,22 @@ pub async fn pk_decrypt( Ok(content) } -/// Symmetric encryption. +/// Symmetric encryption with armored base64 text output. pub async fn symm_encrypt(passphrase: &str, plain: &[u8]) -> Result { + let message = symm_encrypt_to_message(passphrase, plain).await?; + let encoded_msg = message.to_armored_string(None)?; + Ok(encoded_msg) +} + +/// Symmetric encryption with binary output. +pub async fn symm_encrypt_bytes(passphrase: &str, plain: &[u8]) -> Result> { + let message = symm_encrypt_to_message(passphrase, plain).await?; + let mut buf = Vec::new(); + message.to_writer(&mut buf)?; + Ok(buf) +} + +async fn symm_encrypt_to_message(passphrase: &str, plain: &[u8]) -> Result { let lit_msg = Message::new_literal_bytes("", plain); let passphrase = passphrase.to_string(); @@ -332,10 +347,7 @@ pub async fn symm_encrypt(passphrase: &str, plain: &[u8]) -> Result { let s2k = StringToKey::new_default(&mut rng); let msg = lit_msg.encrypt_with_password(&mut rng, s2k, Default::default(), || passphrase)?; - - let encoded_msg = msg.to_armored_string(None)?; - - Ok(encoded_msg) + Ok(msg) }) .await } diff --git a/src/upload.rs b/src/upload.rs index 6e1a58d5d..18f58bf55 100644 --- a/src/upload.rs +++ b/src/upload.rs @@ -1,12 +1,26 @@ use crate::context::Context; use crate::error::{bail, Result}; +use crate::pgp::{symm_decrypt, symm_encrypt_bytes}; +use async_std::fs; use async_std::path::PathBuf; use rand::Rng; +use std::io::Cursor; +use url::Url; /// Upload file to a HTTP upload endpoint. -pub async fn upload_file(_context: &Context, url: String, filepath: PathBuf) -> Result { - // TODO: Use tokens for upload, encrypt file with PGP. - let response = surf::put(url).body_file(filepath)?.await; +pub async fn upload_file( + context: &Context, + url: impl AsRef, + filepath: PathBuf, +) -> Result { + let (passphrase, url) = parse_upload_url(url)?; + + let content = fs::read(filepath).await?; + let encrypted = symm_encrypt_bytes(&passphrase, &content).await?; + + // TODO: Use tokens for upload. + info!(context, "uploading encrypted file to {}", &url); + let response = surf::put(url).body_bytes(encrypted).await; if let Err(err) = response { bail!("Upload failed: {}", err); } @@ -17,16 +31,59 @@ pub async fn upload_file(_context: &Context, url: String, filepath: PathBuf) -> } } +/// Download and decrypt a file from a HTTP endpoint. +/// TODO: Use this. +#[allow(dead_code)] +pub async fn download_file(context: &Context, url: String) -> Result> { + let (passphrase, url) = parse_upload_url(url)?; + info!(context, "downloading file from {}", &url); + let response = surf::get(url).recv_bytes().await; + if let Err(err) = response { + bail!("Download failed: {}", err); + } + let reader = Cursor::new(response.unwrap()); + let decrypted = symm_decrypt(&passphrase, reader).await?; + Ok(decrypted) +} + +/// Parse a URL from a string and take out the hash fragment. +fn parse_upload_url(url: impl AsRef) -> Result<(String, Url)> { + let mut url = url::Url::parse(url.as_ref())?; + let passphrase = url.fragment(); + if passphrase.is_none() { + bail!("Missing passphrase for upload URL"); + } + let passphrase = passphrase.unwrap().to_string(); + url.set_fragment(None); + Ok((passphrase, url)) +} + /// Generate a random URL based on the provided endpoint. -pub fn generate_upload_url(_context: &Context, endpoint: String) -> String { +pub fn generate_upload_url(_context: &Context, mut endpoint: String) -> String { + // equals at least 16 random bytes (base32 takes 160% of binary size). + const FILENAME_LEN: usize = 26; + // equals at least 32 random bytes. + const PASSPHRASE_LEN: usize = 52; + + if endpoint.chars().last() == Some('/') { + endpoint.pop(); + } + let passphrase = generate_token_string(PASSPHRASE_LEN); + let filename = generate_token_string(FILENAME_LEN); + format!("{}/{}#{}", endpoint, filename, passphrase) +} + +/// Generate a random string encoded in base32. +/// Len is the desired string length of the result. +/// TODO: There's likely better methods to create random tokens. +pub fn generate_token_string(len: usize) -> String { const CROCKFORD_ALPHABET: &[u8] = b"0123456789abcdefghjkmnpqrstvwxyz"; - const FILENAME_LEN: usize = 27; let mut rng = rand::thread_rng(); - let filename: String = (0..FILENAME_LEN) + let token: String = (0..len) .map(|_| { let idx = rng.gen_range(0, CROCKFORD_ALPHABET.len()); CROCKFORD_ALPHABET[idx] as char }) .collect(); - format!("{}{}", endpoint, filename) + token }