pingap_util/
crypto.rs

1// Copyright 2024-2025 Tree xie.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use super::Error;
16use super::{base64_decode, base64_encode};
17use aes_gcm_siv::{
18    aead::{Aead, KeyInit},
19    Aes256GcmSiv, Nonce,
20};
21use once_cell::sync::Lazy;
22
23type Result<T, E = Error> = std::result::Result<T, E>;
24
25static PINGAP_NONCE: Lazy<&Nonce> =
26    Lazy::new(|| Nonce::from_slice(b"pingap nonce"));
27
28/// Generates a 32-byte key from the input string.
29/// If the input is longer than 32 bytes, it's truncated.
30/// If shorter, it's padded with zeros.
31///
32/// # Arguments
33/// * `key` - The input string to generate the key from
34///
35/// # Returns
36/// A Vec<u8> containing the 32-byte key
37fn generate_key(key: &str) -> Vec<u8> {
38    let key_size = 32;
39    let buf = key.as_bytes();
40    let pos = buf.len();
41    if pos > key_size {
42        return buf[0..key_size].to_vec();
43    }
44    if pos == key_size {
45        return buf.to_vec();
46    }
47    let mut block: Vec<u8> = vec![0; key_size];
48    block[..pos].copy_from_slice(buf);
49    block
50}
51
52/// Encrypts data using AES-256-GCM-SIV with a static nonce.
53///
54/// # Arguments
55/// * `key` - The encryption key
56/// * `data` - The plaintext data to encrypt
57///
58/// # Returns
59/// * `Ok(String)` - Base64 encoded ciphertext
60/// * `Err(Error)` - If encryption fails
61pub fn aes_encrypt(key: &str, data: &str) -> Result<String> {
62    let cipher =
63        Aes256GcmSiv::new_from_slice(&generate_key(key)).map_err(|e| {
64            Error::Invalid {
65                message: e.to_string(),
66            }
67        })?;
68    let cipher_text =
69        cipher
70            .encrypt(&PINGAP_NONCE, data.as_bytes())
71            .map_err(|e| Error::Aes {
72                message: e.to_string(),
73            })?;
74    Ok(base64_encode(&cipher_text))
75}
76
77/// Decrypts AES-256-GCM-SIV encrypted data using a static nonce.
78///
79/// # Arguments
80/// * `key` - The decryption key
81/// * `data` - Base64 encoded ciphertext to decrypt
82///
83/// # Returns
84/// * `Ok(String)` - Decrypted plaintext
85/// * `Err(Error)` - If decryption or base64 decoding fails
86pub fn aes_decrypt(key: &str, data: &str) -> Result<String> {
87    let cipher =
88        Aes256GcmSiv::new_from_slice(&generate_key(key)).map_err(|e| {
89            Error::Invalid {
90                message: e.to_string(),
91            }
92        })?;
93    let cipher_text =
94        base64_decode(data).map_err(|e| Error::Base64Decode { source: e })?;
95    let plaintext = cipher
96        .decrypt(&PINGAP_NONCE, cipher_text.as_ref())
97        .map_err(|e| Error::Aes {
98            message: e.to_string(),
99        })?;
100
101    Ok(std::str::from_utf8(&plaintext)
102        .unwrap_or_default()
103        .to_string())
104}
105
106#[cfg(test)]
107mod tests {
108    use super::*;
109    use pretty_assertions::assert_eq;
110
111    #[test]
112    fn test_aes_encrypt() {
113        let key = "12345678901234567890123456789012";
114        let data = "hello";
115        let result = aes_encrypt(key, data);
116        assert_eq!(result.is_ok(), true);
117
118        let result = aes_decrypt(key, &result.unwrap());
119        assert_eq!(result.is_ok(), true);
120        assert_eq!(result.unwrap(), data);
121    }
122}