Add S3 encryption
This commit is contained in:
@@ -23,6 +23,7 @@ pub struct S3DataSource {
|
||||
pub prefix: Option<SmartString<LazyCompact>>,
|
||||
pub client: Arc<aws_sdk_s3::Client>,
|
||||
pub pattern: GroupPattern,
|
||||
pub encryption_key: Option<[u8; 32]>,
|
||||
pub index: OnceLock<HashMap<SmartString<LazyCompact>, Item>>,
|
||||
}
|
||||
|
||||
@@ -35,6 +36,7 @@ impl S3DataSource {
|
||||
region: String,
|
||||
credentials: &S3Credentials,
|
||||
pattern: GroupPattern,
|
||||
encryption_key: Option<[u8; 32]>,
|
||||
) -> Result<Arc<Self>, std::io::Error> {
|
||||
let client = {
|
||||
let creds = Credentials::new(
|
||||
@@ -63,6 +65,7 @@ impl S3DataSource {
|
||||
prefix: prefix.map(|x| x.into()),
|
||||
client: Arc::new(client),
|
||||
pattern,
|
||||
encryption_key,
|
||||
index: OnceLock::new(),
|
||||
});
|
||||
|
||||
@@ -94,8 +97,15 @@ impl S3DataSource {
|
||||
|
||||
for obj in resp.contents() {
|
||||
let Some(full_key) = obj.key() else { continue };
|
||||
let key = strip_prefix(full_key, source.prefix.as_deref());
|
||||
all_keys.insert(key.into());
|
||||
let raw_key = strip_prefix(full_key, source.prefix.as_deref());
|
||||
let key = match &source.encryption_key {
|
||||
None => raw_key.into(),
|
||||
Some(enc_key) => match decrypt_path(enc_key, raw_key) {
|
||||
Some(decrypted) => decrypted.into(),
|
||||
None => continue,
|
||||
},
|
||||
};
|
||||
all_keys.insert(key);
|
||||
}
|
||||
|
||||
if !is_truncated {
|
||||
@@ -219,6 +229,50 @@ impl DataSource for Arc<S3DataSource> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Derive an encryption key from a password
|
||||
pub fn string_to_key(password: &str) -> [u8; 32] {
|
||||
blake3::derive_key("pile s3 encryption", password.as_bytes())
|
||||
}
|
||||
|
||||
/// Encrypt a logical path to a base64 S3 key using a deterministic nonce.
|
||||
pub fn encrypt_path(enc_key: &[u8; 32], path: &str) -> String {
|
||||
use base64::Engine;
|
||||
use chacha20poly1305::{KeyInit, XChaCha20Poly1305, XNonce, aead::Aead};
|
||||
|
||||
let hash = blake3::keyed_hash(enc_key, path.as_bytes());
|
||||
let nonce_bytes = &hash.as_bytes()[..24];
|
||||
let nonce = XNonce::from_slice(nonce_bytes);
|
||||
let key = chacha20poly1305::Key::from_slice(enc_key);
|
||||
let cipher = XChaCha20Poly1305::new(key);
|
||||
#[expect(clippy::expect_used)]
|
||||
let ciphertext = cipher
|
||||
.encrypt(nonce, path.as_bytes())
|
||||
.expect("path encryption should not fail");
|
||||
|
||||
let mut result = nonce_bytes.to_vec();
|
||||
result.extend_from_slice(&ciphertext);
|
||||
base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(result)
|
||||
}
|
||||
|
||||
/// Decrypt a base64 S3 key back to its logical path.
|
||||
fn decrypt_path(enc_key: &[u8; 32], encrypted: &str) -> Option<String> {
|
||||
use base64::Engine;
|
||||
use chacha20poly1305::{KeyInit, XChaCha20Poly1305, XNonce, aead::Aead};
|
||||
|
||||
let bytes = base64::engine::general_purpose::URL_SAFE_NO_PAD
|
||||
.decode(encrypted)
|
||||
.ok()?;
|
||||
if bytes.len() < 24 + 16 {
|
||||
return None;
|
||||
}
|
||||
let (nonce_bytes, ciphertext) = bytes.split_at(24);
|
||||
let nonce = XNonce::from_slice(nonce_bytes);
|
||||
let key = chacha20poly1305::Key::from_slice(enc_key);
|
||||
let cipher = XChaCha20Poly1305::new(key);
|
||||
let plaintext = cipher.decrypt(nonce, ciphertext).ok()?;
|
||||
String::from_utf8(plaintext).ok()
|
||||
}
|
||||
|
||||
fn strip_prefix<'a>(key: &'a str, prefix: Option<&str>) -> &'a str {
|
||||
match prefix {
|
||||
None => key,
|
||||
|
||||
Reference in New Issue
Block a user