Rust implementations of the Senzing encryption plugin interface. Each plugin compiles to a C-compatible shared library (.so) that Senzing loads at runtime via dlopen. Two plugins are included: an AES-256-CBC reference plugin (deterministic — fixed IV in both modes) and a dummy XOR plugin for development. Both serve as reference implementations for writing your own — see Security caveats before deploying either to production.
cargo build --release --workspaceOutput: target/release/libsz_aes_encrypt_plugin.so and target/release/libsz_dummy_encrypt_plugin.so
AES plugin:
export SZ_AES_KEY="0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" # 64 hex chars (32 bytes)
export SZ_AES_IV="0123456789abcdef0123456789abcdef" # 32 hex chars (16 bytes)Dummy plugin:
export SZ_DUMMY_KEY="44554d4d595f584f525f763130" # any even number of hex charsThe dummy plugin also accepts an empty key for passthrough mode — XOR and
base64 are skipped, output is "ENC:" + plaintext verbatim:
export SZ_DUMMY_KEY=""This is intended for test fixtures where you want the encryption pipeline exercised end-to-end without transforming on-disk bytes — e.g. mirroring a cleartext-style reference plugin where corpus expected values are plaintext. The env var must be set but empty; unset still errors out so the operator can't accidentally no-op the plugin by forgetting to configure it.
Point Senzing at the plugin library and ensure it can find it:
export LD_LIBRARY_PATH=/path/to/target/release:$LD_LIBRARY_PATHcargo test --workspaceNo environment variables needed — tests use direct initialization to avoid env::set_var race conditions.
The dummy plugin (sz_dummy_plugin) is the simplest reference. Use it as your starting point. A plugin consists of three things:
- A struct that implements
EncryptionProvider - A one-line macro invocation that generates the C FFI
- A
Cargo.tomlthat builds acdylib
sz_my_plugin/
├── Cargo.toml
└── src/
├── lib.rs
├── my_encryption.rs
└── c_interface.rs
Add to root Cargo.toml:
[workspace]
members = [
"sz_common",
"sz_my_plugin",
]Your Cargo.toml:
[package]
name = "sz_my_plugin"
version = "0.1.0"
edition = "2024"
[lib]
name = "sz_my_encrypt_plugin"
crate-type = ["cdylib", "rlib"] # cdylib = shared library for C, rlib = for Rust tests
[dependencies]
sz_common = { workspace = true }
libc = { workspace = true }
zeroize = { workspace = true }
# ... your crypto dependenciesThe trait (sz_common::traits) defines the contract Senzing expects:
| Method | Purpose |
|---|---|
init(&mut self) |
Read configuration (typically env vars), set up keys. Called once. |
close(&mut self) |
Zeroize keys and release resources. Called once. |
signature(&self) |
Return a static string identifying your algorithm (e.g. "MY_ALG_v1"). |
validate_signature(&self, sig) |
Return Ok(()) if sig matches your signature, else error. |
encrypt(&self, plaintext) |
Non-deterministic encryption (random IV each call). |
encrypt_deterministic(&self, plaintext) |
Same plaintext always produces same ciphertext. |
decrypt(&self, ciphertext) |
Reverse of encrypt. |
decrypt_deterministic(&self, ciphertext) |
Reverse of encrypt_deterministic. |
Your struct must also implement Default (the macro uses it to construct instances).
Minimal example (see sz_dummy_plugin/src/dummy_encryption.rs for the full version):
use sz_common::{EncryptionProvider, EncryptionError, Result, parse_hex_string};
use zeroize::Zeroize;
pub struct MyEncryption {
key: Vec<u8>,
}
impl Default for MyEncryption {
fn default() -> Self { Self { key: Vec::new() } }
}
impl EncryptionProvider for MyEncryption {
fn init(&mut self) -> Result<()> {
let key_hex = std::env::var("MY_KEY")
.map_err(|_| EncryptionError::InitializationFailed {
message: "MY_KEY environment variable not set".to_string(),
})?;
self.key = parse_hex_string(&key_hex, "MY_KEY")?;
Ok(())
}
fn close(&mut self) -> Result<()> {
self.key.zeroize();
Ok(())
}
fn signature(&self) -> &'static str { "MY_ALG_v1" }
fn validate_signature(&self, sig: &str) -> Result<()> {
if sig == self.signature() { Ok(()) }
else { Err(EncryptionError::InvalidSignature { signature: sig.to_string() }) }
}
fn encrypt(&self, plaintext: &str) -> Result<String> { todo!() }
fn encrypt_deterministic(&self, plaintext: &str) -> Result<String> { todo!() }
fn decrypt(&self, ciphertext: &str) -> Result<String> { todo!() }
fn decrypt_deterministic(&self, ciphertext: &str) -> Result<String> { todo!() }
}
impl Drop for MyEncryption {
fn drop(&mut self) { self.key.zeroize(); }
}Key rules:
- All encrypted output should be prefixed with
ENC:(usesz_common::add_encryption_prefix) - Decrypt must handle the
ENC:prefix (usesz_common::remove_encryption_prefix) - Use
sz_common::parse_hex_stringfor hex key parsing instead of rolling your own - Implement
Dropto zeroize key material
c_interface.rs — this is the entire file:
use crate::my_encryption::MyEncryption;
sz_common::declare_c_interface!(MyEncryption);The macro generates all 8 extern "C" functions that Senzing expects (G2Encryption_InitPlugin, G2Encryption_ClosePlugin, etc.). It handles:
- Global singleton state via
OnceLock<Mutex<Option<T>>> - Thread-safe access via
Mutex - C string conversion and error buffer population
- Mapping
EncryptionErrorvariants to C error codes
mod c_interface;
mod my_encryption;
pub use c_interface::*;
pub use my_encryption::MyEncryption;
pub use sz_common::{EncryptionError, EncryptionProvider, Result};
pub const MY_SIGNATURE: &str = "MY_ALG_v1";Add an init_with_key method gated behind #[cfg(test)] so tests don't need env::set_var:
#[cfg(test)]
pub fn init_with_key(&mut self, key_hex: &str) -> Result<()> {
self.key = parse_hex_string(key_hex, "key")?;
Ok(())
}Build and verify:
cargo build --release -p sz_my_plugin
cargo test -p sz_my_pluginThree layers, each with a single responsibility:
┌─────────────────────────────────────────────────┐
│ sz_common │
│ ├── EncryptionProvider trait (the contract) │
│ ├── declare_c_interface! macro (C FFI glue) │
│ ├── EncryptionError (error types + C codes) │
│ └── utils (hex parsing, string conversion) │
└─────────────────────────────────────────────────┘
▲ ▲
│ implements trait │ invokes macro
┌─────────┴──────────┐ ┌─────────┴──────────┐
│ sz_aes_plugin │ │ sz_dummy_plugin │
│ AesEncryption │ │ DummyEncryption │
│ (AES-256-CBC) │ │ (XOR cipher) │
└────────────────────┘ └────────────────────┘
Each plugin compiles to an independent .so with no runtime dependency on the other.
sz_rust_encrypt_plugin/
├── Cargo.toml # Workspace root
├── deny.toml # License policy
├── include/
│ └── sz_encrypt_plugin.h # Shared C header (all plugins export the same interface)
├── sz_common/src/
│ ├── traits.rs # EncryptionProvider trait
│ ├── c_interface_macro.rs # declare_c_interface! macro
│ ├── errors.rs # EncryptionError + C error codes
│ └── utils.rs # parse_hex_string, C string helpers
├── sz_aes_plugin/src/
│ ├── aes_encryption.rs # AES-256-CBC implementation
│ └── c_interface.rs # One-line macro invocation
├── sz_dummy_plugin/src/
│ ├── dummy_encryption.rs # XOR implementation
│ └── c_interface.rs # One-line macro invocation
├── examples/
│ ├── test_aes_plugin.c # C integration test
│ └── test_dummy_plugin.c # C integration test
└── CMakeLists.txt # Builds C examples against the .so files
All plugins export the same 8 functions. See include/sz_encrypt_plugin.h for the full declarations.
| Function | Purpose |
|---|---|
G2Encryption_InitPlugin |
Initialize plugin, read config |
G2Encryption_ClosePlugin |
Clean up and release resources |
G2Encryption_GetSignature |
Return algorithm identifier |
G2Encryption_ValidateSignatureCompatibility |
Check if a signature is ours |
G2Encryption_EncryptDataField |
Non-deterministic encrypt |
G2Encryption_DecryptDataField |
Decrypt |
G2Encryption_EncryptDataFieldDeterministic |
Deterministic encrypt |
G2Encryption_DecryptDataFieldDeterministic |
Deterministic decrypt |
All functions return int64_t: 0 on success, negative error code on failure.
Per the Senzing encryption plugin spec:
| Code | Constant | Meaning |
|---|---|---|
| 0 | G2_ENCRYPTION_PLUGIN___SUCCESS |
Success |
| -1 | G2_ENCRYPTION_PLUGIN___SIMPLE_ERROR |
Recoverable error |
| -5 | G2_ENCRYPTION_PLUGIN___OUTPUT_BUFFER_SIZE_ERROR |
Output buffer too small |
| -20 | G2_ENCRYPTION_PLUGIN___CRITICAL_ERROR |
Critical error (e.g. not initialized) |
| -30 | G2_ENCRYPTION_PLUGIN___FAILED_SIGNATURE_VALIDATION |
Signature mismatch |
| Plugin | Signature | Environment Variables |
|---|---|---|
| AES | AES256_CBC_v1.0 |
SZ_AES_KEY, SZ_AES_IV |
| Dummy | DUMMY_XOR_v1.0 |
SZ_DUMMY_KEY |
- AES plugin: AES-256-CBC with PKCS#7 padding. Key material is zeroized on close and drop. The included implementation uses a fixed IV for both deterministic and non-deterministic modes (delegates
encrypttoencrypt_deterministic) — a real deployment should use random IVs for non-deterministic mode. - Dummy plugin: XOR cipher. Not cryptographically secure. Use only for development and testing.
Apache-2.0. See LICENSE.