use base64::engine::general_purpose::STANDARD as B64;
use base64::Engine as _;
use rainier_crypt::CryptScheme;
use rainier_framework::config::{Config as ConfigRepository, Env};
use rainier_framework::prelude::*;
use rainier_framework::testing::TestApp;
use std::sync::Arc;
const KEY: &str = "base64:AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=";
async fn app(scheme: CryptScheme) -> Arc<Application> {
Rainier::new(".")
.without_tracing()
.without_facades()
.configure(move |config| {
config.set(rainier_framework::keys::APP_CIPHER, scheme).unwrap();
})
.boot()
.await
.expect("boots")
}
fn encryption(scheme: CryptScheme) -> Encryption {
let keys = rainier_crypt::KeyRing::from_base64(KEY, &[]).expect("a key");
match scheme {
CryptScheme::Native => Encryption::from_keys(keys),
CryptScheme::Php => Encryption::new(
Arc::new(rainier_crypt::PhpEncrypter::new(keys.clone())),
Arc::new(rainier_crypt::HmacSigner::new(keys)),
),
}
}
#[tokio::test]
async fn the_php_scheme_writes_the_php_envelope() {
let crypt = encryption(CryptScheme::Php);
let payload = crypt.encrypt("a card number").unwrap();
let decoded = B64.decode(&payload).expect("base64");
let json: serde_json::Value = serde_json::from_slice(&decoded).expect("json");
assert!(json["iv"].is_string());
assert!(json["value"].is_string());
assert!(json["mac"].is_string());
assert_eq!(crypt.decrypt(&payload).unwrap(), "a card number");
}
#[tokio::test]
async fn the_native_scheme_writes_something_else_entirely() {
let payload = encryption(CryptScheme::Native).encrypt("a card number").unwrap();
assert!(B64
.decode(&payload)
.ok()
.and_then(|raw| serde_json::from_slice::<serde_json::Value>(&raw).ok())
.is_none());
}
#[tokio::test]
async fn the_two_schemes_cannot_read_each_other() {
let native = encryption(CryptScheme::Native);
let php = encryption(CryptScheme::Php);
let by_native = native.encrypt("secret").unwrap();
let by_php = php.encrypt("secret").unwrap();
assert!(php.decrypt(&by_native).is_err());
assert!(native.decrypt(&by_php).is_err());
}
#[tokio::test]
async fn the_default_is_the_native_scheme() {
let app = app(CryptScheme::default()).await;
let config = app.resolve::<ConfigRepository>().unwrap();
assert_eq!(config.setting(rainier_framework::keys::APP_CIPHER).unwrap(), CryptScheme::Native);
}
#[tokio::test]
async fn the_setting_reaches_the_bound_encryption() {
let app = app(CryptScheme::Php).await;
let crypt = app.resolve::<Encryption>().unwrap();
let payload = crypt.encrypt("a card number").unwrap();
let decoded = B64.decode(&payload).expect("base64 around JSON");
assert!(serde_json::from_slice::<serde_json::Value>(&decoded).is_ok());
}
#[tokio::test]
async fn a_scheme_nobody_can_spell_stops_the_boot() {
let env = Env::parse("APP_CIPHER=hpp");
assert!(env.setting::<CryptScheme>("APP_CIPHER").is_err());
}
#[tokio::test]
async fn a_ported_application_can_still_boot_and_serve() {
let booted = Rainier::new(".")
.without_tracing()
.configure(|config| {
config.set(rainier_framework::keys::APP_CIPHER, CryptScheme::Php).unwrap();
})
.with_routes(|router| {
router.get("/", || async { Response::ok("up") });
})
.boot()
.await
.expect("boots on the PHP cipher");
TestApp::new(booted).unwrap().get("/").await.assert_ok();
}