dsrp 0.5.4 copy "dsrp: ^0.5.4" to clipboard
dsrp: ^0.5.4 copied to clipboard

Secure Remote Password (SRP-6a) for zero-knowledge authentication without transmitting password-equivalents.

dsrp #

pub package License

dsrp is a pure-Dart implementation of the Secure Remote Password (SRP-6a) protocol for secure user authentication, compatible with all Dart and Flutter platforms.

SRP allows password-based authentication without transmitting password-equivalent information to the server, protecting against man-in-the-middle attacks and server database breaches.

Table of Contents #

Features #

  • Zero-knowledge password proof - Server never receives password or password-equivalent data.
  • Pure Dart - Works on all Dart platforms (Flutter mobile, web, desktop, and server).
  • Mutual authentication - Both client and server verify each other's identity.
  • Secure session keys - Generates shared symmetric keys for encrypted communication to optionally supplement other encryption layers such as TLS.
  • RFC5054 compliant - compatible with SRP-6a, the most widely adopted standard for SRP.
  • Customizable cryptography - Built-in support for multiple hash functions (SHA256, SHA512, SHA1) and KDFs (Argon2id, PBKDF2), or bring your own custom functions tailored to your use case.
  • Custom safe primes - Generate your own primes to reduce vulnerability to pre-computed attacks.
  • Memory security - Uses Uint8List for passwords with secure erasure via overwriteWithZeros().
  • Python interoperability - Fully compatible with pysrp library.

Platform Support #

  • ✅ Flutter (Android, iOS, Desktop, Web)
  • ✅ All Dart platforms

Installation #

Add dsrp to your pubspec.yaml:

dependencies:
  dsrp: ^0.5.4

Then run:

dart pub get

If using with Flutter, you can add the cryptography_flutter package for platform-specific performance optimizations:

dependencies:
  cryptography_flutter: ^2.3.2

Usage #

SRP authentication consists of two phases, registration and authentication:

Client                            Server
   |                                 |
   | --- 1. Registration ----------> | (stores salt + verifier)
   |                                 |
   | <-- 2a. Challenge ------------- | (ephemeral key B)
   |                                 |
   | --- 2b. User Proof -----------> | (ephemeral key A + M1)
   |                                 |
   | <-- 2c. Server Proof ---------- | (M2)
   |                                 |
   | === Mutual Authentication ===== | (shared session key)

1. Registration Phase #

The user creates a salted verification key from their credentials and sends it to the server for storage:

import 'package:dsrp/dsrp.dart';

// User creates salted verification key from password
final saltedVerificationKey = await User.createSaltedVerificationKey(
  userId: 'alice',
  password: 'secure-password-123',
);

// Send saltedVerificationKey.key and saltedVerificationKey.salt to server.
// Server stores these for future authentication.

2. Authentication Phase #

The user and server mutually authenticate and derive a shared session key, all without the user ever sending their password to the server:

// 1. User requests challenge from server (sends userId).
final server = Server(
  userId: 'alice',
  salt: saltedVerificationKey.salt,        // Retrieved from database
  verifierKey: saltedVerificationKey.key,  // Retrieved from database
);
final challenge = await server.createChallenge();

// 2. User processes challenge and generates session verifiers.
final user = await User.fromUserCredsAndChallenge(
  userId: 'alice',
  password: 'secure-password-123',
  challenge: challenge,
);
final userSessionVerifiers = user.getUserSessionVerifiers();

// 3. Server verifies user and generates server verifier.
final serverSessionKeyVerifier = await server.verifySession(
  ephemeralUserPublicKey: userSessionVerifiers.ephemeralUserPublicKey,
  userSessionKeyVerifier: userSessionVerifiers.sessionKeyVerifier,
);

// 4. User verifies server (throws exception if verification fails).
await user.verifySession(serverSessionKeyVerifier);

// 5. Both parties now have identical symmetric session keys for encrypted communication.
final userSessionKey = user.sessionKey;
final serverSessionKey = server.sessionKey;
// userSessionKey == serverSessionKey

Note: Session keys supplement but do not replace TLS encryption. Always use TLS for transport security.

A complete working example is in example/dsrp.dart:

dart example/dsrp.dart

Advanced Usage #

Custom Hash Functions and KDFs #

If the built-in hash functions or KDFs do not meet your needs, you can provide your own by implementing the HashFunction and Kdf interfaces:

import 'dart:typed_data';
import 'package:dsrp/dsrp.dart';
import 'package:dsrp/crypto/hash.dart';
import 'package:dsrp/crypto/kdf.dart';

// Custom hash function example
class CustomBlake3Hash implements HashFunction {
  @override
  String get name => 'blake3';

  @override
  Future<Uint8List> hash(Uint8List input) async {
    // Your custom hash implementation.
    // ...
  }
}

// Custom KDF example
class CustomScryptKdf implements Kdf {
  @override
  String get name => 'scrypt';

  @override
  Future<Uint8List> deriveKeyFromPasswordBytes({
    required Uint8List passwordBytes,
    required Uint8List salt,
    Uint8List? userIdBytes,
  }) async {
    // Your custom KDF implementation.
    // ...
  }
}

// Usage with custom implementations.

// Registration with custom KDF.
final customKdf = CustomScryptKdf();
final saltedKey = await User.createSaltedVerificationKeyFromBytes(
  userIdBytes: 'alice'.utf8Bytes,
  passwordBytes: passwordBytes,
  customKdf: customKdf,  // Use custom KDF instead of built-in
);

// Authentication with custom hash function.
final customHash = CustomBlake3Hash();

// Server creates challenge with custom hash
final server = Server(
  userId: 'alice',
  salt: saltedKey.salt,
  verifierKey: saltedKey.key,
  customHashFunction: customHash,  // Use custom hash instead of built-in
);
final challenge = await server.createChallenge();

// User processes challenge with matching custom hash and KDF
final user = await User.fromUserCredsBytesAndChallenge(
  userIdBytes: 'alice'.utf8Bytes,
  passwordBytes: passwordBytes,
  challenge: challenge,
  customHashFunction: customHash,  // Must match server's hash function
  customKdf: customKdf,
);

Important:

  • The name property must uniquely identify your implementation.
  • Hash functions must match between client and server.
  • KDFs must match between registration and authentication.
  • Custom implementations allow integration with specialized cryptographic libraries or hardware security modules.

Security Best Practices #

1. Generate Custom Safe Primes (Critical for Production) #

⚠️ Default primes are NOT recommended for production use.

The default safe prime from RFC5054 may be vulnerable to pre-computed attacks. For production deployments, generate your own custom safe primes.

See scripts/generate_safe_primes for details.

2. Use Strong Key Derivation Functions #

The default KDF is Argon2id, which is memory-hard and recommended for password-based key derivation. Alternative KDFs (PBKDF2-SHA256, PBKDF2-SHA512) are available but less secure against brute-force attacks.

3. SRP Complements, Not Replaces, TLS #

SRP provides authentication and session key derivation. Always use TLS or similar for transport encryption. SRP session keys can be used for additional application-layer encryption if needed.

4. Handle Sensitive Data Securely #

Best Practice: Use Uint8List for passwords instead of String to enable secure memory erasure.

Unlike String (which is immutable), Uint8List can be zeroed out after use to prevent sensitive data from lingering in memory:

// Method 1: Use the utf8Bytes extension for convenience.
final passwordBytes = password.utf8Bytes;
password = ""; // Clear string reference for garbage collection.

final saltedKey = await User.createSaltedVerificationKeyFromBytes(
  userIdBytes: 'alice'.utf8Bytes,
  passwordBytes: passwordBytes,
);

// Zero out sensitive data when done. Does not rely on GC timing.
passwordBytes.overwriteWithZeros();
saltedKey.erase();

// Method 2: String-based API (simpler but potentially less secure).
final saltedKey = await User.createSaltedVerificationKey(
  userId: 'alice',
  password: 'secure-password-123', // String persists until GC
);

Key principles:

  • Convert strings to bytes as early as possible.
  • Remove references to strings as soon as possible to allow them to be garbage collected, e.g. password = "" or password = null.
  • Zero out byte arrays when no longer needed using .overwriteWithZeros().
  • Use .erase() methods on SRP objects (SaltedVerificationKey, Challenge, UserSessionVerifiers) when they are no longer needed.
  • Strings cannot be securely erased like byte arrays can - use byte-based APIs (createSaltedVerificationKeyFromBytes, fromUserCredsBytesAndChallenge) for maximum security, especially if strings can be avoided entirely.

Sensitive data examples:

  • Passwords
  • Private keys
  • User IDs (if privacy-sensitive)

See SECURITY.md for more security tips.

Performance and Profiling #

See PROFILING.md for a performance profiling and optimization guide, including how to optimize Argon2id parameters via benchmarking.

Additional Resources #

Contributions #

License #

Apache 2.0 - See LICENSE for details.

1
likes
150
points
135
downloads

Publisher

verified publisherfictiontoreality.dev

Weekly Downloads

Secure Remote Password (SRP-6a) for zero-knowledge authentication without transmitting password-equivalents.

Repository
View/report issues

Topics

#security #cryptography #authentication #srp #password

Documentation

API reference

License

Apache-2.0 (license)

Dependencies

cryptography, logging

More

Packages that depend on dsrp