use aes_gcm::aead::{Aead, KeyInit, Payload};
use aes_gcm::{Aes256Gcm, Nonce};
use base64::{Engine as _, engine::general_purpose::STANDARD as B64};
use md5::Md5;
use rsa::{Pkcs1v15Encrypt, RsaPublicKey};
use sha2::{Digest, Sha256};
use crate::error::{Result, UgosError};
const IV_LEN: usize = 12;
const TAG_LEN: usize = 16;
#[derive(Debug, Clone)]
pub struct RequestKey {
key: [u8; 32],
pub security_code: String,
}
impl RequestKey {
pub fn generate(public_key: &RsaPublicKey) -> Result<Self> {
let mut raw = [0u8; 16];
rsa::rand_core::RngCore::fill_bytes(&mut rsa::rand_core::OsRng, &mut raw);
let hex = hex::encode(raw);
let mut key = [0u8; 32];
key.copy_from_slice(hex.as_bytes());
let mut rng = rsa::rand_core::OsRng;
let wrapped = public_key
.encrypt(&mut rng, Pkcs1v15Encrypt, hex.as_bytes())
.map_err(|e| UgosError::Encryption(format!("wrapping request key: {e}")))?;
Ok(Self {
key,
security_code: B64.encode(wrapped),
})
}
pub fn encrypt(&self, plaintext: &str) -> Result<String> {
let cipher = Aes256Gcm::new_from_slice(&self.key)
.map_err(|e| UgosError::Encryption(format!("AES key: {e}")))?;
let mut iv = [0u8; IV_LEN];
rsa::rand_core::RngCore::fill_bytes(&mut rsa::rand_core::OsRng, &mut iv);
let nonce = Nonce::from(iv);
let sealed = cipher
.encrypt(
&nonce,
Payload {
msg: plaintext.as_bytes(),
aad: &[],
},
)
.map_err(|e| UgosError::Encryption(format!("AES encrypt: {e}")))?;
let mut out = Vec::with_capacity(IV_LEN + sealed.len());
out.extend_from_slice(&iv);
out.extend_from_slice(&sealed);
Ok(B64.encode(out))
}
pub fn decrypt(&self, encoded: &str) -> Result<String> {
let raw = B64
.decode(encoded)
.map_err(|e| UgosError::Encryption(format!("base64 decode: {e}")))?;
if raw.len() < IV_LEN + TAG_LEN {
return Err(UgosError::Encryption("encrypted payload too short".into()));
}
let cipher = Aes256Gcm::new_from_slice(&self.key)
.map_err(|e| UgosError::Encryption(format!("AES key: {e}")))?;
let nonce = Nonce::try_from(&raw[..IV_LEN])
.map_err(|e| UgosError::Encryption(format!("nonce: {e}")))?;
let plain = cipher
.decrypt(
&nonce,
Payload {
msg: &raw[IV_LEN..],
aad: &[],
},
)
.map_err(|e| UgosError::Encryption(format!("AES decrypt: {e}")))?;
String::from_utf8(plain).map_err(|e| UgosError::Encryption(format!("not UTF-8: {e}")))
}
}
pub fn rsa_seal(public_key: &RsaPublicKey, value: &str) -> Result<String> {
let mut rng = rsa::rand_core::OsRng;
let sealed = public_key
.encrypt(&mut rng, Pkcs1v15Encrypt, value.as_bytes())
.map_err(|e| UgosError::Encryption(format!("RSA seal: {e}")))?;
Ok(B64.encode(sealed))
}
#[must_use]
pub fn sha256_hex(input: &str) -> String {
let mut hasher = Sha256::new();
hasher.update(input.as_bytes());
hex::encode(hasher.finalize())
}
#[must_use]
pub fn md5_hex(input: &str) -> String {
let mut hasher = Md5::new();
hasher.update(input.as_bytes());
hex::encode(hasher.finalize())
}
#[must_use]
pub fn query_string(params: &[(&str, &str)]) -> String {
params
.iter()
.map(|(k, v)| format!("{}={}", urlencode(k), urlencode(v)))
.collect::<Vec<_>>()
.join("&")
}
#[must_use]
pub fn urlencode_component(value: &str) -> String {
urlencode(value)
}
fn urlencode(value: &str) -> String {
const HEX: &[u8; 16] = b"0123456789ABCDEF";
let mut out = String::with_capacity(value.len());
for byte in value.bytes() {
match byte {
b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => {
out.push(byte as char);
}
_ => {
out.push('%');
out.push(HEX[(byte >> 4) as usize] as char);
out.push(HEX[(byte & 0x0f) as usize] as char);
}
}
}
out
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
#[test]
fn round_trip() {
let key = RequestKey {
key: *b"0123456789abcdef0123456789abcdef",
security_code: String::new(),
};
let sealed = key.encrypt(r#"{"path":"/volume1"}"#).unwrap();
assert_eq!(key.decrypt(&sealed).unwrap(), r#"{"path":"/volume1"}"#);
}
#[test]
fn layout_is_iv_ciphertext_tag() {
let key = RequestKey {
key: *b"0123456789abcdef0123456789abcdef",
security_code: String::new(),
};
let sealed = B64.decode(key.encrypt("x").unwrap()).unwrap();
assert_eq!(sealed.len(), IV_LEN + 1 + TAG_LEN);
}
#[test]
fn sha256_matches_cryptojs_hex() {
assert_eq!(
sha256_hex("abc"),
"ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"
);
}
#[test]
fn md5_matches_reference_vectors() {
assert_eq!(md5_hex(""), "d41d8cd98f00b204e9800998ecf8427e");
assert_eq!(md5_hex("abc"), "900150983cd24fb0d6963f7d28e17f72");
}
#[test]
fn query_string_encodes_paths() {
assert_eq!(
query_string(&[("path", "/volume1/a b"), ("page", "1")]),
"path=%2Fvolume1%2Fa%20b&page=1"
);
}
}