A Stellar-oriented Proxy Re-Encryption (PRE) library in Rust with asymmetric key isolation:
| Role | Keys | Use |
|---|---|---|
| Alice (delegator) | S... → HKDF(info) → pre_sk / pre_pk |
Encryption target, self-decrypt, re-encryption key generation |
| Bob (delegatee) | Public G...; signing scalar from S... |
Rekey target; decrypt re-encrypted ciphertexts |
Bob does not run HKDF. Rekey only needs his public G.... His S... is used only locally for decryption and is never sent as part of rekey generation.
- Encrypt to Alice’s published
pre_pk(not bareG...) - Alice decrypts with
S...(internalpre_sk) rekey_gen(Alice_S, Bob_G)produces a re-encryption key- Proxy re-encrypts without learning the plaintext
- Bob decrypts re-encrypted data with
S...(signing scalar)
use stellar_recrypt::{
decrypt, decrypt_reencrypted, encrypt, reencrypt, rekey_gen, structured_info,
StellarKeyPair,
};
use rand_core::OsRng;
fn main() {
// Required HKDF domain separation — pick a distinct info per purpose / recipient.
let info = structured_info(b"demo-succession-v1", &[]);
let alice = StellarKeyPair::generate(&mut OsRng, &info);
let bob = StellarKeyPair::generate(&mut OsRng, &info);
// Encryptors use Alice's published pre_pk
let ct = encrypt(&mut OsRng, &alice.pre_public, b"succession secret").unwrap();
assert_eq!(decrypt(&alice.secret, &ct).unwrap(), b"succession secret");
// Alice: pre_sk + Bob's G...
let rk = rekey_gen(&mut OsRng, &alice.secret, &bob.stellar_public).unwrap();
let reenc = reencrypt(&rk, &ct).unwrap();
// Bob decrypts locally with S...
assert_eq!(
decrypt_reencrypted(&bob.secret, &reenc).unwrap(),
b"succession secret"
);
}cargo test
cargo run --example basicAlice S... seed
├─ Ed25519 ──► G_A // account identity only
└─ HKDF(info) ► pre_sk_A
pre_pk_A // published encryption target
Bob
G_B ── rekey target (public)
S_B ── local signing scalar to decrypt re-encrypted ciphertext
HKDF parameters:
HKDF-SHA256(
ikm = stellar_seed,
salt = "StellarRecrypt-v1",
info = <caller-provided, required>,
L = 64
) → pre_sk
There is no library default for info. Callers must pass it explicitly when
constructing Alice keys (from_seed, from_strkey, generate, PrePublicKey::from_*).
Use structured_info to compose purpose and peer fields safely:
structured_info(arg_a, arg_b)
→ "pre-encryption-scalar" || 0x00 || arg_a' || 0x00 || arg_b'
If an argument is itself a prior structured_info output, it is unwrapped one layer
first (PREFIX || PAD || x || PAD || y restores to x || PAD || y). Both arguments may
be empty: structured_info(&[], &[]) → PREFIX || PAD || PAD.
use stellar_recrypt::{
decrypt, decrypt_reencrypted, encrypt, reencrypt, rekey_gen, structured_info,
PrePublicKey, StellarKeyPair, StellarSecretKey,
};
use rand_core::OsRng;
let alice_seed = *StellarKeyPair::generate(&mut OsRng, &structured_info(b"seed-gen", &[]))
.secret
.as_seed_bytes();
let bob = StellarKeyPair::generate(&mut OsRng, &structured_info(b"bob-gen", &[]));
// Purpose + Bob's Ed25519 pubkey → isolated pre_sk / pre_pk for this pair
let info = structured_info(b"rekey", bob.stellar_public.as_ed25519_bytes());
let alice = StellarSecretKey::from_seed(&alice_seed, &info).unwrap();
let pre_pk = PrePublicKey::from_stellar_seed(&alice_seed, &info).unwrap();
let ct = encrypt(&mut OsRng, &pre_pk, b"only for this pair path").unwrap();
assert_eq!(decrypt(&alice, &ct).unwrap(), b"only for this pair path");
let rk = rekey_gen(&mut OsRng, &alice, &bob.stellar_public).unwrap();
let reenc = reencrypt(&rk, &ct).unwrap();
assert_eq!(decrypt_reencrypted(&bob.secret, &reenc).unwrap(), b"only for this pair path");Use the same info when deriving pre_sk and pre_pk. Bob still does not run HKDF;
he decrypts re-encrypted data with his signing scalar from S....
| Threat | Outcome |
|---|---|
Bob colludes with a proxy holding rk |
Can recover pre_sk_A, not Alice’s signing seed / funds |
| Proxy alone | Cannot learn plaintext or private keys |
Rekey with G_B |
Does not leak S_B |
| Bob’s decryption key | Same lineage as signing; compromise of Bob’s secret affects his account |
This is a simplified single-hop, non-threshold PRE. It does not provide cryptographic collusion-safety in the pairing/AFGH sense. Assess independently before production use.
HKDF info is mandatory (no Option, no library default). This is intentional:
-
Why force callers to pass
info?
If a shared default existed, most applications would use it and Alice would have a single globalpre_sk/pre_pk. Collusion that recovers that scalar (Bob + proxy withrk) would then decrypt all ciphertexts under the defaultpre_pk, across every unrelated purpose or recipient. Requiring an explicitinfonudges callers to choose different contexts, limiting blast radius. -
What collusion still does
Domain separation does not prevent collusion. Bob + proxy can still recover thepre_skfor the sameinfoused to build that rekey. Isolation is per-info, not collusion-proof. -
Caller guidance
- Prefer different
infoper purpose and/or per recipient
(e.g.structured_info(b"succession-v1", bob_pk)). - Always use the same
infowhen deriving matchingpre_skandpre_pk. - Do not reuse one global string across unrelated apps, tenants, or recipients.
- Empty or constant
inforeintroduces cross-context blast radius. - Bob’s re-encrypted decrypt uses only the signing scalar;
decrypt_reencrypted_with_strkeydoes not takeinfo. If you build Bob’s key withfrom_strkeyyourself, any explicit placeholder is fine.
- Prefer different
| Type / function | Description |
|---|---|
PrePublicKey |
Alice’s encryption public key (32-byte compressed point) |
StellarPublicKey |
Bob’s G... |
StellarSecretKey |
S...; holds pre_sk + signing scalar; from_seed(seed, info) |
StellarKeyPair |
secret + stellar_public + pre_public; generate(rng, info) |
structured_info(a, b) |
Structured HKDF info: PREFIX || 0x00 || a || 0x00 || b |
encrypt(rng, &pre_pk, msg) |
Encrypt to Alice |
decrypt(&alice_sk, &ct) |
Alice decrypt |
rekey_gen(rng, &alice_sk, &bob_g) |
Generate rk |
reencrypt(&rk, &ct) |
Proxy re-encrypt |
decrypt_reencrypted(&bob_sk, &reenc) |
Bob decrypt |
decrypt_with_strkey(alice_s, info, ct) |
Alice decrypt via strkey |
rekey_gen_strkey(rng, alice_s, info, bob_g) |
Rekey via strkeys |
decrypt_reencrypted_with_strkey(bob_s, reenc) |
Bob decrypt via strkey (no info) |
Licensed under either of:
at your option.
Copyright (c) 2026 Cele.Kln