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 | ||||
|     AgeVerifyName(String, String, String), // name, message, signature_b64 | ||||
|     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 { | ||||
| @@ -623,6 +629,20 @@ impl 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()), | ||||
|                     }, | ||||
|                     protocol, | ||||
| @@ -737,6 +757,12 @@ impl Cmd { | ||||
|             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::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))), | ||||
|         } | ||||
|     } | ||||
|   | ||||
| @@ -1,4 +1,5 @@ | ||||
| pub mod age;   // NEW | ||||
| pub mod sym; | ||||
| pub mod cmd; | ||||
| pub mod crypto; | ||||
| 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