mirror of
https://github.com/chatmail/core.git
synced 2026-05-03 21:36:29 +03:00
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.
This commit is contained in:
22
src/pgp.rs
22
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<String> {
|
||||
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<Vec<u8>> {
|
||||
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<Message> {
|
||||
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<String> {
|
||||
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
|
||||
}
|
||||
|
||||
@@ -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<String> {
|
||||
// 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<str>,
|
||||
filepath: PathBuf,
|
||||
) -> Result<String> {
|
||||
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<Vec<u8>> {
|
||||
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<str>) -> 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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user