mirror of
https://github.com/chatmail/core.git
synced 2026-05-20 15:26:30 +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,
|
SignedPublicSubKey, SignedSecretKey, SubkeyParamsBuilder,
|
||||||
};
|
};
|
||||||
use pgp::crypto::{HashAlgorithm, SymmetricKeyAlgorithm};
|
use pgp::crypto::{HashAlgorithm, SymmetricKeyAlgorithm};
|
||||||
|
use pgp::ser::Serialize;
|
||||||
use pgp::types::{
|
use pgp::types::{
|
||||||
CompressionAlgorithm, KeyTrait, Mpi, PublicKeyTrait, SecretKeyTrait, StringToKey,
|
CompressionAlgorithm, KeyTrait, Mpi, PublicKeyTrait, SecretKeyTrait, StringToKey,
|
||||||
};
|
};
|
||||||
@@ -322,8 +323,22 @@ pub async fn pk_decrypt(
|
|||||||
Ok(content)
|
Ok(content)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Symmetric encryption.
|
/// Symmetric encryption with armored base64 text output.
|
||||||
pub async fn symm_encrypt(passphrase: &str, plain: &[u8]) -> Result<String> {
|
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 lit_msg = Message::new_literal_bytes("", plain);
|
||||||
let passphrase = passphrase.to_string();
|
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 s2k = StringToKey::new_default(&mut rng);
|
||||||
let msg =
|
let msg =
|
||||||
lit_msg.encrypt_with_password(&mut rng, s2k, Default::default(), || passphrase)?;
|
lit_msg.encrypt_with_password(&mut rng, s2k, Default::default(), || passphrase)?;
|
||||||
|
Ok(msg)
|
||||||
let encoded_msg = msg.to_armored_string(None)?;
|
|
||||||
|
|
||||||
Ok(encoded_msg)
|
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,26 @@
|
|||||||
use crate::context::Context;
|
use crate::context::Context;
|
||||||
use crate::error::{bail, Result};
|
use crate::error::{bail, Result};
|
||||||
|
use crate::pgp::{symm_decrypt, symm_encrypt_bytes};
|
||||||
|
use async_std::fs;
|
||||||
use async_std::path::PathBuf;
|
use async_std::path::PathBuf;
|
||||||
use rand::Rng;
|
use rand::Rng;
|
||||||
|
use std::io::Cursor;
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
/// Upload file to a HTTP upload endpoint.
|
/// Upload file to a HTTP upload endpoint.
|
||||||
pub async fn upload_file(_context: &Context, url: String, filepath: PathBuf) -> Result<String> {
|
pub async fn upload_file(
|
||||||
// TODO: Use tokens for upload, encrypt file with PGP.
|
context: &Context,
|
||||||
let response = surf::put(url).body_file(filepath)?.await;
|
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 {
|
if let Err(err) = response {
|
||||||
bail!("Upload failed: {}", err);
|
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.
|
/// 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 CROCKFORD_ALPHABET: &[u8] = b"0123456789abcdefghjkmnpqrstvwxyz";
|
||||||
const FILENAME_LEN: usize = 27;
|
|
||||||
let mut rng = rand::thread_rng();
|
let mut rng = rand::thread_rng();
|
||||||
let filename: String = (0..FILENAME_LEN)
|
let token: String = (0..len)
|
||||||
.map(|_| {
|
.map(|_| {
|
||||||
let idx = rng.gen_range(0, CROCKFORD_ALPHABET.len());
|
let idx = rng.gen_range(0, CROCKFORD_ALPHABET.len());
|
||||||
CROCKFORD_ALPHABET[idx] as char
|
CROCKFORD_ALPHABET[idx] as char
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
format!("{}{}", endpoint, filename)
|
token
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user