Skip to main content
Glama
crypto.rs19.7 kB
use serde_json::{ Value, json, }; use si_crypto::{ SensitiveStrings, VeritechDecryptionKey, VeritechDecryptionKeyError, VeritechEncryptionKey, }; use thiserror::Error; const MARKER_FIELD: &str = "cycloneEncryptedDataMarker"; const KEY_HASH_FIELD: &str = "keyHash"; const CRYPTED_FIELD: &str = "crypted"; #[derive(Debug, Error)] pub enum VeritechValueEncryptError { #[error("invalid json pointer: {0}")] InvalidJSONPointer(String), #[error("error serializing to json: {0}")] Serialize(#[from] serde_json::Error), } #[derive(Debug, Error)] pub enum VeritechValueDecryptError { #[error("object missing crypted field")] CryptedFieldMissing, #[error("object crypted field value was not a string")] CryptedFieldValueNotString, #[error("cyclone decryption error: {0}")] VeritechDecryption(#[from] VeritechDecryptionKeyError), #[error("error deserializing from json: {0}")] Deserialize(#[from] serde_json::Error), #[error("invalid json pointer: {0}")] InvalidJSONPointer(String), #[error("object missing key hash field")] KeyHashFieldMissing, #[error("object key hash field value was not a string")] KeyHashFieldValueNotString, #[error("object key hash field value does not match provided decryption key")] KeyHashNoMatch, #[error("object missing marker field")] MarkerFieldMissing, #[error("object marker field value was not a bool")] MarkerFieldValueNotBool, #[error("object marker field value was not true")] MarkerFieldValueNotTrue, #[error("json value was not an object")] ValueNotObject, } pub fn encrypt_value_tree( value: &mut Value, encryption_key: &VeritechEncryptionKey, ) -> Result<(), VeritechValueEncryptError> { let mut json_pointer_stack = vec!["".to_owned()]; while let Some(pointer) = json_pointer_stack.pop() { match value.pointer_mut(&pointer) { Some(value) => match value { Value::String(_) => { let encrypted_value = encrypt_value(value, encryption_key)?; *value = encrypted_value; } Value::Array(array) => { json_pointer_stack.extend( array .iter() .enumerate() .map(|(index, _element)| format!("{pointer}/{index}")), ); } Value::Object(object) => { json_pointer_stack.extend( object .iter() .map(|(key, _value)| format!("{pointer}/{key}")), ); } Value::Null | Value::Bool(_) | Value::Number(_) => { // Nothing to do } }, None => return Err(VeritechValueEncryptError::InvalidJSONPointer(pointer)), } } Ok(()) } pub fn decrypt_value_tree( value: &mut Value, sensitive_strings: &mut SensitiveStrings, decryption_key: &VeritechDecryptionKey, ) -> Result<(), VeritechValueDecryptError> { let mut json_pointer_stack = vec!["".to_owned()]; while let Some(pointer) = json_pointer_stack.pop() { match value.pointer_mut(&pointer) { Some(value) => match value { Value::Array(array) => { json_pointer_stack.extend( array .iter() .enumerate() .map(|(index, _element)| format!("{pointer}/{index}")), ); } Value::Object(_) if is_value_encrypted(value) => { let decrypted_value = decrypt_value(value, decryption_key)?; if let Value::String(sensitive_str) = &decrypted_value { sensitive_strings.insert(sensitive_str); } *value = decrypted_value; } Value::Object(object) => { json_pointer_stack.extend( object .iter() .map(|(key, _value)| format!("{pointer}/{key}")), ); } Value::Null | Value::Bool(_) | Value::Number(_) | Value::String(_) => { // Nothing to do } }, None => return Err(VeritechValueDecryptError::InvalidJSONPointer(pointer)), } } Ok(()) } fn is_value_encrypted(value: &Value) -> bool { match value { Value::Object(value) => { value.get(MARKER_FIELD).is_some() && value.get(KEY_HASH_FIELD).is_some() && value.get(CRYPTED_FIELD).is_some() } Value::Null | Value::Bool(_) | Value::Number(_) | Value::String(_) | Value::Array(_) => { false } } } fn encrypt_value( value: &Value, encryption_key: &VeritechEncryptionKey, ) -> Result<Value, serde_json::Error> { let bytes = serde_json::to_vec(value)?; let crypted = encryption_key.encrypt_and_encode(bytes); Ok(json!({ MARKER_FIELD: true, KEY_HASH_FIELD: encryption_key.key_hash().to_string(), CRYPTED_FIELD: crypted, })) } fn decrypt_value( value: &Value, decryption_key: &VeritechDecryptionKey, ) -> Result<Value, VeritechValueDecryptError> { // Confirm value is an object let value = value .as_object() .ok_or(VeritechValueDecryptError::ValueNotObject)?; // Check marker field is set and is `true` if !value .get(MARKER_FIELD) .ok_or(VeritechValueDecryptError::MarkerFieldMissing)? .as_bool() .ok_or(VeritechValueDecryptError::MarkerFieldValueNotBool)? { return Err(VeritechValueDecryptError::MarkerFieldValueNotTrue); } // Confirm that key hash field value matches hash for provided decryption key if value .get(KEY_HASH_FIELD) .ok_or(VeritechValueDecryptError::KeyHashFieldMissing)? .as_str() .ok_or(VeritechValueDecryptError::KeyHashFieldValueNotString)? != decryption_key.encryption_key_hash_str() { return Err(VeritechValueDecryptError::KeyHashNoMatch); } // Decrypt crypted field and deserialize decrypted contents as a JSON value let decrypted = { let crypted = value .get(CRYPTED_FIELD) .ok_or(VeritechValueDecryptError::CryptedFieldMissing)? .as_str() .ok_or(VeritechValueDecryptError::CryptedFieldValueNotString)?; let bytes = decryption_key.decode_and_decrypt(crypted)?; serde_json::from_slice::<Value>(&bytes)? }; Ok(decrypted) } #[cfg(test)] mod tests { use super::*; mod is_value_encrypted { use super::*; #[test] fn null() { assert!(!is_value_encrypted(&json!(null))); } #[test] fn bool() { assert!(!is_value_encrypted(&json!(true))); assert!(!is_value_encrypted(&json!(false))); } #[test] fn number() { assert!(!is_value_encrypted(&json!(1))); assert!(!is_value_encrypted(&json!(0))); assert!(!is_value_encrypted(&json!(-3.56))); } #[test] fn string() { assert!(!is_value_encrypted(&json!(""))); assert!(!is_value_encrypted(&json!("false"))); assert!(!is_value_encrypted(&json!("ponies"))); } #[test] fn array() { assert!(!is_value_encrypted(&json!([]))); assert!(!is_value_encrypted(&json!(["nope"]))); assert!(!is_value_encrypted(&json!([{"one": "two"}]))); } #[test] fn regular_object() { assert!(!is_value_encrypted(&json!({}))); assert!(!is_value_encrypted(&json!({"one": "two"}))); } #[test] fn encrypted_object() { assert!(is_value_encrypted(&json!({ "cycloneEncryptedDataMarker": true, "keyHash": "abc123", "crypted": "sssshhh", }))); } #[test] fn encrypted_object_missing_marker() { assert!(!is_value_encrypted(&json!({ "keyHash": "abc123", "crypted": "sssshhh", }))); } #[test] fn encrypted_object_missing_key_hash() { assert!(!is_value_encrypted(&json!({ "cycloneEncryptedDataMarker": true, "crypted": "sssshhh", }))); } } mod encrypt_value { use si_crypto::VeritechKeyPair; use super::*; #[test] fn string_round_trip() { let (encryption_key, decryption_key) = VeritechKeyPair::create(); let message = json!("Telling the Bees"); let encrypted_value = encrypt_value(&message, &encryption_key).expect("failed to encrypt value"); let decrypted_message = decrypt_value(&encrypted_value, &decryption_key) .expect("failed to decrypt message"); assert_eq!(message, decrypted_message); } #[test] fn obj_round_trip() { let (encryption_key, decryption_key) = VeritechKeyPair::create(); let message = json!({ "artist": "Dream Theater", "album": "Train of Thought", "songTitle": "As I Am", "styles": [ "Art Rock", "Heavy Metal", "Neo-Prog", "Progressive Metal", ], }); let encrypted_value = encrypt_value(&message, &encryption_key).expect("failed to encrypt value"); let decrypted_message = decrypt_value(&encrypted_value, &decryption_key) .expect("failed to decrypt message"); assert_eq!(message, decrypted_message); } } mod decrypt_value { use si_crypto::VeritechKeyPair; use super::*; fn encrypted(encryption_key: &VeritechEncryptionKey) -> Value { encrypt_value(&json!("my-secret"), encryption_key).expect("failed to encrypt value") } #[test] fn value_not_object() { let (_encryption_key, decryption_key) = VeritechKeyPair::create(); assert!(matches!( decrypt_value(&json!("uh oh"), &decryption_key), Err(VeritechValueDecryptError::ValueNotObject), )); } #[test] fn marker_field_missing() { let (encryption_key, decryption_key) = VeritechKeyPair::create(); let mut encrypted = encrypted(&encryption_key); encrypted .as_object_mut() .expect("value is not an object") .remove(MARKER_FIELD); assert!(matches!( decrypt_value(&encrypted, &decryption_key), Err(VeritechValueDecryptError::MarkerFieldMissing), )); } #[test] fn marker_field_value_not_bool() { let (encryption_key, decryption_key) = VeritechKeyPair::create(); let mut encrypted = encrypted(&encryption_key); *encrypted .as_object_mut() .expect("value is not an object") .get_mut(MARKER_FIELD) .expect("missing field") = json!("not a bool"); assert!(matches!( decrypt_value(&encrypted, &decryption_key), Err(VeritechValueDecryptError::MarkerFieldValueNotBool), )); } #[test] fn marker_field_value_not_true() { let (encryption_key, decryption_key) = VeritechKeyPair::create(); let mut encrypted = encrypted(&encryption_key); *encrypted .as_object_mut() .expect("value is not an object") .get_mut(MARKER_FIELD) .expect("missing field") = json!(false); assert!(matches!( decrypt_value(&encrypted, &decryption_key), Err(VeritechValueDecryptError::MarkerFieldValueNotTrue), )); } #[test] fn key_hash_field_missing() { let (encryption_key, decryption_key) = VeritechKeyPair::create(); let mut encrypted = encrypted(&encryption_key); encrypted .as_object_mut() .expect("value is not an object") .remove(KEY_HASH_FIELD); assert!(matches!( decrypt_value(&encrypted, &decryption_key), Err(VeritechValueDecryptError::KeyHashFieldMissing), )); } #[test] fn key_hash_field_value_not_str() { let (encryption_key, decryption_key) = VeritechKeyPair::create(); let mut encrypted = encrypted(&encryption_key); *encrypted .as_object_mut() .expect("value is not an object") .get_mut(KEY_HASH_FIELD) .expect("missing field") = json!(true); assert!(matches!( decrypt_value(&encrypted, &decryption_key), Err(VeritechValueDecryptError::KeyHashFieldValueNotString), )); } #[test] fn key_hash_field_value_no_match() { let (encryption_key, decryption_key) = VeritechKeyPair::create(); let (wrong_encryption_key, _) = VeritechKeyPair::create(); let mut encrypted = encrypted(&encryption_key); *encrypted .as_object_mut() .expect("value is not an object") .get_mut(KEY_HASH_FIELD) .expect("missing field") = json!(wrong_encryption_key.key_hash().to_string()); assert!(matches!( decrypt_value(&encrypted, &decryption_key), Err(VeritechValueDecryptError::KeyHashNoMatch), )); } #[test] fn crypted_field_missing() { let (encryption_key, decryption_key) = VeritechKeyPair::create(); let mut encrypted = encrypted(&encryption_key); encrypted .as_object_mut() .expect("value is not an object") .remove(CRYPTED_FIELD); assert!(matches!( decrypt_value(&encrypted, &decryption_key), Err(VeritechValueDecryptError::CryptedFieldMissing), )); } #[test] fn crypted_field_value_not_str() { let (encryption_key, decryption_key) = VeritechKeyPair::create(); let mut encrypted = encrypted(&encryption_key); *encrypted .as_object_mut() .expect("value is not an object") .get_mut(CRYPTED_FIELD) .expect("missing field") = json!(true); assert!(matches!( decrypt_value(&encrypted, &decryption_key), Err(VeritechValueDecryptError::CryptedFieldValueNotString), )); } #[test] fn crypted_field_value_not_properly_encoded() { let (encryption_key, decryption_key) = VeritechKeyPair::create(); let mut encrypted = encrypted(&encryption_key); *encrypted .as_object_mut() .expect("value is not an object") .get_mut(CRYPTED_FIELD) .expect("missing field") = json!("uh oh"); assert!(matches!( decrypt_value(&encrypted, &decryption_key), Err(VeritechValueDecryptError::VeritechDecryption(_)), )); } #[test] fn wrong_decryption_key() { let (encryption_key, _) = VeritechKeyPair::create(); let (_, wrong_decryption_key) = VeritechKeyPair::create(); let encrypted = encrypted(&encryption_key); assert!(matches!( dbg!(decrypt_value(&encrypted, &wrong_decryption_key)), Err(VeritechValueDecryptError::KeyHashNoMatch), )); } } mod encrypt_value_tree { use std::collections::HashSet; use si_crypto::VeritechKeyPair; use si_std::SensitiveString; use super::*; #[test] fn object_with_string_field_values_round_trip() { let (encryption_key, decryption_key) = VeritechKeyPair::create(); let mut sensitive_strings = SensitiveStrings::default(); let mut secret = json!({ "username": "Mike Portnoy", "password": "Drummer", }); let expected_secret = secret.clone(); encrypt_value_tree(&mut secret, &encryption_key).expect("failed to encrypt tree"); decrypt_value_tree(&mut secret, &mut sensitive_strings, &decryption_key) .expect("failed to decrypt tree"); assert_eq!(expected_secret, secret); let strings: HashSet<SensitiveString> = sensitive_strings.into(); assert_eq!(2, strings.len()); assert!(strings.contains(&"Mike Portnoy".into())); assert!(strings.contains(&"Drummer".into())); } #[test] fn nested_object_round_trip() { let (encryption_key, decryption_key) = VeritechKeyPair::create(); let mut sensitive_strings = SensitiveStrings::default(); let mut secret = json!({ "strValue": "i-am-a-string", "boolValue": false, "nestedObjValue": { "again": { "stuff": null, "token": "token-1-2-3", "groceries": ["milk", "cheese", "brains"], }, }, "arrValue": [1, 2, 3], "nullValue": null, "numberValue": -1, }); let expected_secret = secret.clone(); encrypt_value_tree(&mut secret, &encryption_key).expect("failed to encrypt tree"); assert!(is_value_encrypted( secret .pointer("/strValue") .expect("failed to find strValue value") )); assert!(is_value_encrypted( secret .pointer("/nestedObjValue/again/token") .expect("failed to find token value") )); assert!(is_value_encrypted( secret .pointer("/nestedObjValue/again/groceries/2") .expect("failed to find groceries/2 value") )); decrypt_value_tree(&mut secret, &mut sensitive_strings, &decryption_key) .expect("failed to decrypt tree"); assert_eq!(expected_secret, secret); let strings: HashSet<SensitiveString> = sensitive_strings.into(); assert_eq!(5, strings.len()); assert!(strings.contains(&"milk".into())); assert!(strings.contains(&"cheese".into())); assert!(strings.contains(&"brains".into())); assert!(strings.contains(&"token-1-2-3".into())); assert!(strings.contains(&"i-am-a-string".into())); } } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/systeminit/si'

If you have feedback or need assistance with the MCP directory API, please join our Discord server