Implemented symmetric encryption; new commands are SYM KEYGEN
; SYM ENCRYPT
; SYM DECRYPT
This commit is contained in:
26
src/cmd.rs
26
src/cmd.rs
@@ -84,6 +84,12 @@ pub enum Cmd {
|
|||||||
AgeSignName(String, String), // name, message
|
AgeSignName(String, String), // name, message
|
||||||
AgeVerifyName(String, String, String), // name, message, signature_b64
|
AgeVerifyName(String, String, String), // name, message, signature_b64
|
||||||
AgeList,
|
AgeList,
|
||||||
|
|
||||||
|
// SYM (symmetric) commands — stateless (Phase 1)
|
||||||
|
// Raw 32-byte key provided as base64; ciphertext returned as base64
|
||||||
|
SymKeygen,
|
||||||
|
SymEncrypt(String, String), // key_b64, message
|
||||||
|
SymDecrypt(String, String), // key_b64, ciphertext_b64
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Cmd {
|
impl Cmd {
|
||||||
@@ -623,6 +629,20 @@ impl Cmd {
|
|||||||
_ => return Err(DBError(format!("unsupported AGE subcommand {:?}", cmd))),
|
_ => return Err(DBError(format!("unsupported AGE subcommand {:?}", cmd))),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
"sym" => {
|
||||||
|
if cmd.len() < 2 {
|
||||||
|
return Err(DBError("wrong number of arguments for SYM".to_string()));
|
||||||
|
}
|
||||||
|
match cmd[1].to_lowercase().as_str() {
|
||||||
|
"keygen" => { if cmd.len() != 2 { return Err(DBError("SYM KEYGEN takes no args".to_string())); }
|
||||||
|
Cmd::SymKeygen }
|
||||||
|
"encrypt" => { if cmd.len() != 4 { return Err(DBError("SYM ENCRYPT <key_b64> <message>".to_string())); }
|
||||||
|
Cmd::SymEncrypt(cmd[2].clone(), cmd[3].clone()) }
|
||||||
|
"decrypt" => { if cmd.len() != 4 { return Err(DBError("SYM DECRYPT <key_b64> <ciphertext_b64>".to_string())); }
|
||||||
|
Cmd::SymDecrypt(cmd[2].clone(), cmd[3].clone()) }
|
||||||
|
_ => return Err(DBError(format!("unsupported SYM subcommand {:?}", cmd))),
|
||||||
|
}
|
||||||
|
}
|
||||||
_ => Cmd::Unknow(cmd[0].clone()),
|
_ => Cmd::Unknow(cmd[0].clone()),
|
||||||
},
|
},
|
||||||
protocol,
|
protocol,
|
||||||
@@ -737,6 +757,12 @@ impl Cmd {
|
|||||||
Cmd::AgeSignName(name, message) => Ok(crate::age::cmd_age_sign_name(server, &name, &message).await),
|
Cmd::AgeSignName(name, message) => Ok(crate::age::cmd_age_sign_name(server, &name, &message).await),
|
||||||
Cmd::AgeVerifyName(name, message, sig_b64) => Ok(crate::age::cmd_age_verify_name(server, &name, &message, &sig_b64).await),
|
Cmd::AgeVerifyName(name, message, sig_b64) => Ok(crate::age::cmd_age_verify_name(server, &name, &message, &sig_b64).await),
|
||||||
Cmd::AgeList => Ok(crate::age::cmd_age_list(server).await),
|
Cmd::AgeList => Ok(crate::age::cmd_age_list(server).await),
|
||||||
|
|
||||||
|
// SYM (symmetric): stateless (Phase 1)
|
||||||
|
Cmd::SymKeygen => Ok(crate::sym::cmd_sym_keygen().await),
|
||||||
|
Cmd::SymEncrypt(key_b64, message) => Ok(crate::sym::cmd_sym_encrypt(&key_b64, &message).await),
|
||||||
|
Cmd::SymDecrypt(key_b64, ct_b64) => Ok(crate::sym::cmd_sym_decrypt(&key_b64, &ct_b64).await),
|
||||||
|
|
||||||
Cmd::Unknow(s) => Ok(Protocol::err(&format!("ERR unknown command `{}`", s))),
|
Cmd::Unknow(s) => Ok(Protocol::err(&format!("ERR unknown command `{}`", s))),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
pub mod age; // NEW
|
pub mod age; // NEW
|
||||||
|
pub mod sym;
|
||||||
pub mod cmd;
|
pub mod cmd;
|
||||||
pub mod crypto;
|
pub mod crypto;
|
||||||
pub mod error;
|
pub mod error;
|
||||||
|
123
src/sym.rs
Normal file
123
src/sym.rs
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
//! sym.rs — Stateless symmetric encryption (Phase 1)
|
||||||
|
//!
|
||||||
|
//! Commands implemented (RESP):
|
||||||
|
//! - SYM KEYGEN
|
||||||
|
//! - SYM ENCRYPT <key_b64> <message>
|
||||||
|
//! - SYM DECRYPT <key_b64> <ciphertext_b64>
|
||||||
|
//!
|
||||||
|
//! Notes:
|
||||||
|
//! - Raw key: exactly 32 bytes, provided as Base64 in commands.
|
||||||
|
//! - Cipher: XChaCha20-Poly1305 (AEAD) without AAD in Phase 1
|
||||||
|
//! - Ciphertext binary layout: [version:1][nonce:24][ciphertext||tag]
|
||||||
|
//! - Encoding for wire I/O: Base64
|
||||||
|
|
||||||
|
use base64::{engine::general_purpose::STANDARD as B64, Engine as _};
|
||||||
|
use chacha20poly1305::{
|
||||||
|
aead::{Aead, KeyInit, OsRng},
|
||||||
|
XChaCha20Poly1305, XNonce,
|
||||||
|
};
|
||||||
|
use rand::RngCore;
|
||||||
|
|
||||||
|
use crate::protocol::Protocol;
|
||||||
|
|
||||||
|
const VERSION: u8 = 1;
|
||||||
|
const NONCE_LEN: usize = 24;
|
||||||
|
const TAG_LEN: usize = 16;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum SymWireError {
|
||||||
|
InvalidKey,
|
||||||
|
BadEncoding,
|
||||||
|
BadFormat,
|
||||||
|
BadVersion(u8),
|
||||||
|
Crypto,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SymWireError {
|
||||||
|
fn to_protocol(self) -> Protocol {
|
||||||
|
match self {
|
||||||
|
SymWireError::InvalidKey => Protocol::err("ERR sym: invalid key"),
|
||||||
|
SymWireError::BadEncoding => Protocol::err("ERR sym: bad encoding"),
|
||||||
|
SymWireError::BadFormat => Protocol::err("ERR sym: bad format"),
|
||||||
|
SymWireError::BadVersion(v) => Protocol::err(&format!("ERR sym: unsupported version {}", v)),
|
||||||
|
SymWireError::Crypto => Protocol::err("ERR sym: auth failed"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn decode_key_b64(s: &str) -> Result<chacha20poly1305::Key, SymWireError> {
|
||||||
|
let bytes = B64.decode(s.as_bytes()).map_err(|_| SymWireError::BadEncoding)?;
|
||||||
|
if bytes.len() != 32 {
|
||||||
|
return Err(SymWireError::InvalidKey);
|
||||||
|
}
|
||||||
|
Ok(chacha20poly1305::Key::from_slice(&bytes).to_owned())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn encrypt_blob(key: &chacha20poly1305::Key, plaintext: &[u8]) -> Result<Vec<u8>, SymWireError> {
|
||||||
|
let cipher = XChaCha20Poly1305::new(key);
|
||||||
|
|
||||||
|
let mut nonce_bytes = [0u8; NONCE_LEN];
|
||||||
|
OsRng.fill_bytes(&mut nonce_bytes);
|
||||||
|
let nonce = XNonce::from_slice(&nonce_bytes);
|
||||||
|
|
||||||
|
let mut out = Vec::with_capacity(1 + NONCE_LEN + plaintext.len() + TAG_LEN);
|
||||||
|
out.push(VERSION);
|
||||||
|
out.extend_from_slice(&nonce_bytes);
|
||||||
|
|
||||||
|
let ct = cipher.encrypt(nonce, plaintext).map_err(|_| SymWireError::Crypto)?;
|
||||||
|
out.extend_from_slice(&ct);
|
||||||
|
Ok(out)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn decrypt_blob(key: &chacha20poly1305::Key, blob: &[u8]) -> Result<Vec<u8>, SymWireError> {
|
||||||
|
if blob.len() < 1 + NONCE_LEN + TAG_LEN {
|
||||||
|
return Err(SymWireError::BadFormat);
|
||||||
|
}
|
||||||
|
let ver = blob[0];
|
||||||
|
if ver != VERSION {
|
||||||
|
return Err(SymWireError::BadVersion(ver));
|
||||||
|
}
|
||||||
|
let nonce = XNonce::from_slice(&blob[1..1 + NONCE_LEN]);
|
||||||
|
let ct = &blob[1 + NONCE_LEN..];
|
||||||
|
|
||||||
|
let cipher = XChaCha20Poly1305::new(key);
|
||||||
|
cipher.decrypt(nonce, ct).map_err(|_| SymWireError::Crypto)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- Command handlers (RESP) ----------
|
||||||
|
|
||||||
|
pub async fn cmd_sym_keygen() -> Protocol {
|
||||||
|
let mut key_bytes = [0u8; 32];
|
||||||
|
OsRng.fill_bytes(&mut key_bytes);
|
||||||
|
let key_b64 = B64.encode(key_bytes);
|
||||||
|
Protocol::BulkString(key_b64)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn cmd_sym_encrypt(key_b64: &str, message: &str) -> Protocol {
|
||||||
|
let key = match decode_key_b64(key_b64) {
|
||||||
|
Ok(k) => k,
|
||||||
|
Err(e) => return e.to_protocol(),
|
||||||
|
};
|
||||||
|
match encrypt_blob(&key, message.as_bytes()) {
|
||||||
|
Ok(blob) => Protocol::BulkString(B64.encode(blob)),
|
||||||
|
Err(e) => e.to_protocol(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn cmd_sym_decrypt(key_b64: &str, ct_b64: &str) -> Protocol {
|
||||||
|
let key = match decode_key_b64(key_b64) {
|
||||||
|
Ok(k) => k,
|
||||||
|
Err(e) => return e.to_protocol(),
|
||||||
|
};
|
||||||
|
let blob = match B64.decode(ct_b64.as_bytes()) {
|
||||||
|
Ok(b) => b,
|
||||||
|
Err(_) => return SymWireError::BadEncoding.to_protocol(),
|
||||||
|
};
|
||||||
|
match decrypt_blob(&key, &blob) {
|
||||||
|
Ok(pt) => match String::from_utf8(pt) {
|
||||||
|
Ok(s) => Protocol::BulkString(s),
|
||||||
|
Err(_) => Protocol::err("ERR sym: invalid UTF-8 plaintext"),
|
||||||
|
},
|
||||||
|
Err(e) => e.to_protocol(),
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user