// crypto.mbt — local encryption (mirrors Python `crypto.py`)
//
// MoonBit has no Fernet binding, so we implement the same dependency-free
// XOR obfuscation that Python falls back to when the `cryptography` package is
// absent. The transform operates on Unicode code points (so CJK content stays
// intact and round-trips exactly) and is its own inverse. When disabled, data
// passes through unchanged — identical to the Python default behaviour.
///|
/// Optional local encryption provider.
pub struct CryptoProvider {
enabled : Bool
passphrase : String
} derive(Debug)
///|
/// Disabled provider: data is stored as-is.
pub fn CryptoProvider::disabled() -> CryptoProvider {
{ enabled: false, passphrase: "" }
}
///|
/// Enabled provider keyed by `passphrase`.
pub fn CryptoProvider::new(passphrase : String) -> CryptoProvider {
{ enabled: true, passphrase }
}
///|
/// Encrypt (XOR obfuscate) a string.
pub fn CryptoProvider::encrypt(self : CryptoProvider, data : String) -> String {
if !self.enabled {
return data
}
self.xor_obfuscate(data)
}
///|
/// Decrypt (XOR obfuscate) a string.
pub fn CryptoProvider::decrypt(self : CryptoProvider, data : String) -> String {
if !self.enabled {
return data
}
self.xor_obfuscate(data)
}
///|
fn xor_char(c : Char, kc : Char) -> Char {
// `Int::to_char` validates the scalar; fall back to U+FFFD only for the
// (extremely rare) case where XOR yields a non-scalar value, keeping the
// transform its own inverse for all normal CJK/ASCII text.
match (c.to_int() ^ kc.to_int()).to_char() {
Some(x) => x
None => Int::unsafe_to_char(0xFFFD)
}
}
///|
fn CryptoProvider::xor_obfuscate(
self : CryptoProvider,
data : String,
) -> String {
if self.passphrase == "" {
return data
}
let key_chars = self.passphrase.iter().to_array()
let kl = key_chars.length()
let out : Array[Char] = []
let mut i = 0
for c in data.iter() {
let kc = key_chars[i % kl]
out.push(xor_char(c, kc))
i = i + 1
}
String::from_array(out)
}