Building secure FHE applications requires attention to several areas.
Key Management
FHE Keys
| Key | Storage | Access |
|---|
| Public Key | Distributed | Anyone can encrypt |
| Private Key | MPC distributed | Threshold decryption only |
The FHE private key is never held by a single party.
User Keys
// Derive X25519 from Solana keypair
const userCrypto = privora.userCrypto(keypair);
// Same keypair = same X25519 key
// Protect Solana keypair = protect recovery ability
Authorization Security
Principle of Least Privilege
// Good: Only authorize what's needed
if order.owner == user_pubkey {
create_decryption_auth(&order.price_ref.hash(), &user_pubkey)?;
}
// Bad: Over-broad authorization
create_decryption_auth(&data_hash, &any_user)?;
Verify Before Authorizing
pub fn authorize_match_result(
match_result: &MatchResult,
user: &Pubkey,
) -> ProgramResult {
// Verify user was part of the match
let user_was_buyer = match_result.buyer == *user;
let user_was_seller = match_result.seller == *user;
if !user_was_buyer && !user_was_seller {
return Err(ProgramError::InvalidArgument);
}
// Only then authorize
create_decryption_auth(&match_result.fill_price_ref.hash(), user)?;
Ok(())
}
Authorization is Permanent
Once an authorization PDA is created, it grants decryption rights permanently. There is no revocation after MPC decryption occurs.
Data Privacy
What’s Encrypted vs Public
| Data | Visibility |
|---|
| Account existence | Public |
| Account owner | Public |
| Hash references | Public |
| Encrypted values | Private |
| Computation results | Private (until authorized) |
Even with encryption, some information leaks:
- Timing: When operations occur
- Frequency: How often values change
- Patterns: Access patterns to accounts
Consider if this metadata is sensitive for your use case.
Client-Side
// Validate before encryption
function validatePrice(price: number): boolean {
return price >= 0 && price <= 255 && Number.isInteger(price);
}
if (!validatePrice(price)) {
throw new Error('Invalid price');
}
const encrypted = privora.encrypt(price, 'u8');
Program-Side
// Verify instruction data format
if instruction_data.len() < 33 {
return Err(ProgramError::InvalidInstructionData);
}
// Verify accounts
if !order_owner.is_signer {
return Err(ProgramError::MissingRequiredSignature);
}
// Verify state
if order.status != OrderStatus::Open {
return Err(ProgramError::InvalidArgument);
}
Recovery Data Security
Storage
// Bad: Store in plain localStorage
localStorage.setItem('recovery', JSON.stringify(recoveryData));
// Better: Encrypt at rest
const encryptedRecovery = await encryptWithDeviceKey(recoveryData);
secureStorage.setItem('recovery', encryptedRecovery);
Transmission
Never transmit recovery data unencrypted. The recovery data allows decryption without MPC.
Common Vulnerabilities
1. Missing Allocator
// Bug: Missing allocator causes OOM
// privora_sdk_program::setup_fhe_allocator!(); // Missing!
// Fix: Always include at crate root
privora_sdk_program::setup_fhe_allocator!();
2. Type Confusion
// Bug: Wrong type interpretation
let price_u64: Encrypted<u64> = EncryptedRef::<u64>::from_hash(hash).load()?;
// But hash was from u8 encryption!
// Fix: Track types carefully, use typed instruction data
3. Missing Signer Checks
// Bug: No signer verification
let owner = next_account_info(accounts_iter)?;
// Anyone can claim to be owner!
// Fix: Always verify signers
if !owner.is_signer {
return Err(ProgramError::MissingRequiredSignature);
}
Audit Checklist