This library uses Flutter Rust Bridge (FRB) with the libsignal-protocol Rust crate.
Key security properties:
- Memory safety is handled by Rust's ownership system
- Cryptographic operations are implemented in libsignal-protocol (Signal's official Rust implementation)
- No manual memory management in Dart - FRB handles all cleanup automatically
- No
dispose()calls needed - Rust drops resources when they go out of scope
With FRB, memory management is handled automatically:
// FRB Architecture - no cleanup needed
final privateKey = PrivateKey.generate();
final signature = privateKey.sign(message: data);
// privateKey is automatically cleaned up when no longer referencedRust's ownership system ensures:
- No use-after-free
- No double-free
- No memory leaks
- Deterministic cleanup
All cryptographic operations and comparisons are handled by Rust's libsignal-protocol, which uses constant-time implementations internally.
Best practice: Avoid comparing cryptographic data in Dart code. Let the library handle it:
// CORRECT - let Rust handle cryptographic verification
final isValid = publicKey.verifySignature(message: data, signature: sig);
// CORRECT - compare public keys using library methods
final keysMatch = key1.compare(other: key2) == 0;
// AVOID - comparing serialized cryptographic data in Dart
if (key1.serialize() == key2.serialize()) { ... } // Not constant-timeIf you must compare bytes in Dart (e.g., for non-cryptographic purposes), use a constant-time implementation from a crypto package like package:crypto.
Always use UTC for cryptographic timestamps:
// Correct - UTC is timezone-independent
final now = DateTime.now().toUtc();
final expiration = DateTime.fromMillisecondsSinceEpoch(ms, isUtc: true);
// WRONG - local time varies by timezone, can cause certificate validation issues
final now = DateTime.now();This affects:
- Certificate validation (SenderCertificate, ServerCertificate)
- Session timestamps
- Key expiration checks
Stores persist sensitive cryptographic state. For production:
// WRONG - testing only, data lost on restart
final sessionStore = InMemorySessionStore();
// CORRECT - production stores persist securely
final sessionStore = SecureSqliteSessionStore(); // Implement yourselfStore security requirements:
- SessionStore - Persist session records (Double Ratchet state)
- IdentityKeyStore - Store identity keys in secure storage (e.g., flutter_secure_storage)
- PreKeyStore - One-time keys, consumed after use
- SignedPreKeyStore - Rotate periodically (e.g., weekly)
- KyberPreKeyStore - Post-quantum keys
- SenderKeyStore - Group session keys
Never expose key material in logs or errors:
// WRONG - exposes key material
print('Generated key: ${privateKey.serialize()}');
throw Exception('Failed with key: $keyBytes');
// CORRECT - no key material in logs
print('Generated new private key');
throw Exception('Key operation failed');Always validate certificates before use:
final isValid = senderCert.validate(
trustRoot: serverTrustRoot,
timestamp: DateTime.now().toUtc(),
);
if (!isValid) {
throw SecurityException('Invalid sender certificate');
}Device IDs and other inputs are validated in Rust, but Dart code should also validate:
// FRB validates in Rust, but Dart code can also check
if (deviceId < 1 || deviceId > 127) {
throw ArgumentError('Device ID must be 1-127');
}Store operations should be properly synchronized:
import 'package:synchronized/synchronized.dart';
class MySessionStore implements SessionStore {
final _lock = Lock();
final Database _db;
@override
Future<void> storeSession(ProtocolAddress address, SessionRecord record) async {
await _lock.synchronized(() async {
await _db.insert('sessions', {
'address': '${address.name()}:${address.deviceId()}',
'record': record.serialize(),
});
});
}
}Always initialize the library before use:
void main() async {
await LibSignal.init(); // Initialize FRB runtime
runApp(MyApp());
}The library provides utilities for zeroing sensitive data in Dart.
// Wrap takes ownership - no extra copy
final secureData = SecureBytes.wrap(sensitiveBytes);
try {
// ... use secureData.bytes ...
} finally {
secureData.dispose(); // Immediate zeroing (recommended)
}
// Copy constructor - original NOT zeroed (caller responsible)
final secureCopy = SecureBytes(sensitiveBytes);
sensitiveBytes.zeroize(); // Zero the original yourselffinal sensitiveList = Uint8List.fromList([...]);
try {
// ... use sensitiveList ...
} finally {
sensitiveList.zeroize(); // Zero all bytes
}Limitations:
- Dart's garbage collector may copy data before zeroing
- These utilities provide defence-in-depth, not absolute security guarantees
- For critical secrets, prefer keeping them in Rust (opaque types)
When reviewing code changes, verify:
- No in-memory stores in production code
- No key material in logs or error messages
-
DateTime.now().toUtc()used for timestamps -
DateTime.fromMillisecondsSinceEpoch()usesisUtc: true - Cryptographic comparisons done via library methods (not raw byte comparison)
- Certificates validated before use
- Store operations properly synchronized
-
LibSignal.init()called before any operations - Sensitive data in Dart uses
SecureBytesorzeroize()extension
These concerns from the old C FFI architecture are now handled automatically:
| Old Concern | Now Handled By |
|---|---|
| FFI pointer management | Rust ownership |
dispose() pattern |
Rust drop semantics |
| Double-free prevention | Rust borrow checker |
| Buffer overflow prevention | Rust bounds checking |
| Use-after-free | Rust ownership |
| Memory zeroing | Rust (zeroize crate in libsignal) |
-
Dart VM memory: Dart's garbage collector may copy data before Rust can zero it. This is a platform limitation, but libsignal's Rust code uses the
zeroizecrate for sensitive data. -
Timing side channels: All cryptographic operations use constant-time implementations in libsignal-protocol (Rust). Avoid comparing cryptographic data directly in Dart.
-
Store persistence: In-memory stores lose all state on app restart. Production apps must implement persistent stores.
After decryption, plaintext is intentionally NOT zeroized because:
- Plaintext is application data - per NIST guidelines, zeroization applies to cryptographic keys and secret data, not application plaintext
- Responsibility transfer - once decrypted, data belongs to the application layer
- Keys ARE zeroized - identity key pairs and session keys are properly zeroized after use
If your application requires plaintext zeroization, implement it at the Dart layer after processing.
If you discover a security vulnerability, please report it privately rather than opening a public issue. Contact the maintainers directly.