A functional TypeScript library for WebAuthn Pseudo-Random Function (PRF) extension with robust error handling. Provides deterministic key derivation from passkeys using monadic error handling with neverthrow.
- π Deterministic Key Derivation: Generate consistent cryptographic keys from passkeys
- π‘οΈ Type-Safe: Full TypeScript support with comprehensive type definitions
- π§© Monadic Error Handling: Built on neverthrow for reliable functional error handling
- π¦ Universal Support: ESM and CommonJS builds included
- π§ Battle-Tested: Uses SimpleWebAuthn for WebAuthn encoding/decoding
- π Modern WebAuthn: Built on the latest WebAuthn PRF extension specification
Install the package from npm:
npm install prf-passkeyOr with yarn:
yarn add prf-passkeyimport {
registerPasskey,
authenticateAndDeriveKey,
textToSalt,
randomChallenge,
randomUserId
} from 'prf-passkey';
// Configure your app
const config = {
rpName: 'My App',
rpId: 'localhost',
userVerification: 'required'
};
// Register a passkey and derive a pseudorandom value
const result = await registerPasskey(config, {
userId: randomUserId(),
userName: 'user@example.com',
userDisplayName: 'User Name',
challenge: randomChallenge(),
salt: textToSalt('my-salt-v1')
})();
// Handle the result
if (result.isOk()) {
console.log('Pseudorandom value:', result.value.keyHex);
} else {
console.error('Failed:', result.error.message);
}This library has a single, focused purpose: extracting pseudorandom values from WebAuthn passkeys using the PRF extension.
The Pseudo-Random Function (PRF) extension allows passkeys to generate deterministic pseudorandom values based on a salt input. This enables:
- Deterministic key derivation
- Consistent encryption keys across sessions
- Secure pseudorandom number generation
The library uses neverthrow's Result<T, E> type for robust error handling:
if (result.isOk()) {
// Access result.value
} else {
// Handle result.error
}Register a new passkey and get a pseudorandom value during registration.
interface RegistrationOptions {
userId: Uint8Array;
userName: string;
userDisplayName: string;
challenge: Uint8Array;
salt: Uint8Array; // Input for PRF
}
// Returns: Result<RegistrationResult, Error>
interface RegistrationResult {
credentialId: ArrayBuffer;
encodedId: string;
derivedKey: ArrayBuffer | null; // PRF output
keyHex: string | null; // PRF output as hex
}Authenticate with an existing passkey and get a pseudorandom value.
interface AuthenticationOptions {
credentialId: Uint8Array;
challenge: Uint8Array;
salt: Uint8Array; // Input for PRF
}
// Returns: Result<AuthenticationResult, Error>
interface AuthenticationResult {
derivedKey: ArrayBuffer; // PRF output
keyHex: string; // PRF output as hex
}textToSalt(text)- Convert text to salt bytes for PRFrandomChallenge()- Generate WebAuthn challengerandomUserId()- Generate user IDformatKeyAsHex()- Convert ArrayBuffer to hex string
import { registerPasskey, textToSalt, randomChallenge, randomUserId } from 'prf-passkey';
const config = { rpName: 'My App', rpId: 'localhost' };
const result = await registerPasskey(config, {
userId: randomUserId(),
userName: 'user@example.com',
userDisplayName: 'User Name',
challenge: randomChallenge(),
salt: textToSalt('my-application-salt-v1') // PRF input
})();
if (result.isOk()) {
console.log('Pseudorandom value:', result.value.keyHex);
// Store credential ID for future use
localStorage.setItem('credentialId', result.value.encodedId);
}import { authenticateAndDeriveKey, base64urlToUint8Array } from 'prf-passkey';
// Retrieve stored credential ID
const encodedCredentialId = localStorage.getItem('credentialId');
const credentialIdResult = base64urlToUint8Array(encodedCredentialId);
if (credentialIdResult.isOk()) {
const result = await authenticateAndDeriveKey(config, {
credentialId: credentialIdResult.value,
challenge: randomChallenge(),
salt: textToSalt('my-application-salt-v1') // Same salt = same pseudorandom value
})();
if (result.isOk()) {
console.log('Same pseudorandom value:', result.value.keyHex);
}
}// Different salts produce different pseudorandom values from the same passkey
const encryptionSalt = textToSalt('encryption-v1');
const signingSalt = textToSalt('signing-v1');
const encryptionResult = await authenticateAndDeriveKey(config, {
credentialId,
challenge: randomChallenge(),
salt: encryptionSalt
})();
const signingResult = await authenticateAndDeriveKey(config, {
credentialId,
challenge: randomChallenge(),
salt: signingSalt // Different salt
})();
// These will be different values
console.log('Encryption PRF:', encryptionResult.value?.keyHex);
console.log('Signing PRF:', signingResult.value?.keyHex);Run the interactive browser demo:
npm run build
python -m http.server 8000 # or any local serverOpen http://localhost:8000/examples/browser-demo.html
- Modern browser with WebAuthn support:
- Chrome 108+
- Safari 16+
- Firefox 113+
- Device with biometric authentication or security key
- HTTPS connection (except localhost)
- CodeQL Analysis: Automated security scanning on every commit
- Dependency Auditing: Regular security audits of all dependencies
- Security Policy: Responsible disclosure process for vulnerabilities
- No Secrets: Library never stores or logs sensitive data
- 96%+ Code Coverage: Comprehensive test suite with high coverage
- Cross-Platform CI: Tested on Ubuntu, Windows, and macOS
- Multi-Node Support: Compatible with Node.js 18, 20, and 22
- 52 Test Cases: Unit, integration, and error handling tests
- TypeScript First: Full type safety with comprehensive definitions
- ESLint + Security Rules: Automated code quality and security checks
- Automated Releases: Consistent builds and releases via GitHub Actions
- Dependency Management: Automated updates via Dependabot
- Battle-Tested Dependencies: Built on neverthrow and SimpleWebAuthn
- Multiple Formats: ESM and CommonJS builds included
- Tree Shakeable: Optimized for modern bundlers
- Zero Runtime Dependencies: Minimal bundle size impact
- Always use HTTPS in production
- Store credential IDs securely (not in localStorage)
- Use unique salts for different key purposes
- Consider salt versioning for key rotation
- Validate all inputs on the server side
- Fork the repository
- Create a feature branch
- Make your changes
- Add tests
- Submit a pull request
MIT License - see LICENSE.md for details.