first commit
This commit is contained in:
@@ -0,0 +1,112 @@
|
||||
use aes::{Aes128, cipher::{BlockDecrypt, BlockEncrypt, KeyInit, generic_array::GenericArray}};
|
||||
use aes_gcm::{Aes128Gcm, Nonce, aead::{AeadInPlace, KeyInit as AeadKeyInit}};
|
||||
use anyhow::{anyhow, bail, Context, Result};
|
||||
use base64::{engine::general_purpose::STANDARD, Engine};
|
||||
use rand::RngCore;
|
||||
|
||||
pub const GENERIC_GREE_KEY: &str = "a3K8Bx%2r8Y7cB!a";
|
||||
|
||||
pub fn normalize_key(key: &str) -> Result<[u8; 16]> {
|
||||
let bytes = key.as_bytes();
|
||||
if bytes.len() == 16 {
|
||||
let mut out = [0_u8; 16];
|
||||
out.copy_from_slice(bytes);
|
||||
return Ok(out);
|
||||
}
|
||||
if let Ok(decoded) = STANDARD.decode(key) {
|
||||
if decoded.len() == 16 {
|
||||
let mut out = [0_u8; 16];
|
||||
out.copy_from_slice(&decoded);
|
||||
return Ok(out);
|
||||
}
|
||||
}
|
||||
bail!("GREE key must contain 16 bytes or base64-encoded 16 bytes")
|
||||
}
|
||||
|
||||
pub fn encrypt_v1(key: &str, plaintext: &[u8]) -> Result<String> {
|
||||
let key = normalize_key(key)?;
|
||||
let cipher = Aes128::new_from_slice(&key).map_err(|_| anyhow!("invalid AES key"))?;
|
||||
let pad = 16 - (plaintext.len() % 16);
|
||||
let mut data = Vec::with_capacity(plaintext.len() + pad);
|
||||
data.extend_from_slice(plaintext);
|
||||
data.extend(std::iter::repeat(pad as u8).take(pad));
|
||||
for block in data.chunks_exact_mut(16) {
|
||||
cipher.encrypt_block(GenericArray::from_mut_slice(block));
|
||||
}
|
||||
Ok(STANDARD.encode(data))
|
||||
}
|
||||
|
||||
pub fn decrypt_v1(key: &str, ciphertext_b64: &str) -> Result<Vec<u8>> {
|
||||
let key = normalize_key(key)?;
|
||||
let cipher = Aes128::new_from_slice(&key).map_err(|_| anyhow!("invalid AES key"))?;
|
||||
let mut data = STANDARD.decode(ciphertext_b64).context("invalid base64 packet")?;
|
||||
if data.is_empty() || data.len() % 16 != 0 {
|
||||
bail!("invalid AES-ECB ciphertext length")
|
||||
}
|
||||
for block in data.chunks_exact_mut(16) {
|
||||
cipher.decrypt_block(GenericArray::from_mut_slice(block));
|
||||
}
|
||||
let pad = *data.last().ok_or_else(|| anyhow!("empty plaintext"))? as usize;
|
||||
if pad == 0 || pad > 16 || data.len() < pad || data[data.len() - pad..].iter().any(|v| *v as usize != pad) {
|
||||
bail!("invalid PKCS#7 padding")
|
||||
}
|
||||
data.truncate(data.len() - pad);
|
||||
Ok(data)
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct V2Encrypted {
|
||||
pub ciphertext: String,
|
||||
pub nonce: String,
|
||||
pub tag: String,
|
||||
}
|
||||
|
||||
pub fn encrypt_v2(key: &str, plaintext: &[u8]) -> Result<V2Encrypted> {
|
||||
let key = normalize_key(key)?;
|
||||
let cipher = <Aes128Gcm as AeadKeyInit>::new_from_slice(&key).map_err(|_| anyhow!("invalid AES-GCM key"))?;
|
||||
let mut nonce_bytes = [0_u8; 12];
|
||||
rand::thread_rng().fill_bytes(&mut nonce_bytes);
|
||||
let nonce = Nonce::from_slice(&nonce_bytes);
|
||||
let mut buffer = plaintext.to_vec();
|
||||
let tag = cipher.encrypt_in_place_detached(nonce, b"", &mut buffer)
|
||||
.map_err(|_| anyhow!("AES-GCM encryption failed"))?;
|
||||
Ok(V2Encrypted {
|
||||
ciphertext: STANDARD.encode(buffer),
|
||||
nonce: STANDARD.encode(nonce_bytes),
|
||||
tag: STANDARD.encode(tag),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn decrypt_v2(key: &str, ciphertext_b64: &str, nonce_b64: &str, tag_b64: &str) -> Result<Vec<u8>> {
|
||||
let key = normalize_key(key)?;
|
||||
let cipher = <Aes128Gcm as AeadKeyInit>::new_from_slice(&key).map_err(|_| anyhow!("invalid AES-GCM key"))?;
|
||||
let nonce_bytes = STANDARD.decode(nonce_b64).context("invalid GCM nonce")?;
|
||||
if nonce_bytes.len() != 12 { bail!("invalid GCM nonce length") }
|
||||
let tag_bytes = STANDARD.decode(tag_b64).context("invalid GCM tag")?;
|
||||
if tag_bytes.len() != 16 { bail!("invalid GCM tag length") }
|
||||
let mut data = STANDARD.decode(ciphertext_b64).context("invalid GCM ciphertext")?;
|
||||
let nonce = Nonce::from_slice(&nonce_bytes);
|
||||
let tag = GenericArray::from_slice(&tag_bytes);
|
||||
cipher.decrypt_in_place_detached(nonce, b"", &mut data, tag)
|
||||
.map_err(|_| anyhow!("AES-GCM authentication failed"))?;
|
||||
Ok(data)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn v1_round_trip() {
|
||||
let value = br#"{"t":"status","mac":"112233445566"}"#;
|
||||
let encrypted = encrypt_v1(GENERIC_GREE_KEY, value).unwrap();
|
||||
assert_eq!(decrypt_v1(GENERIC_GREE_KEY, &encrypted).unwrap(), value);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn v2_round_trip() {
|
||||
let value = b"gree-gcm-test";
|
||||
let encrypted = encrypt_v2(GENERIC_GREE_KEY, value).unwrap();
|
||||
assert_eq!(decrypt_v2(GENERIC_GREE_KEY, &encrypted.ciphertext, &encrypted.nonce, &encrypted.tag).unwrap(), value);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user