@mentaproject/react-native-passkey-signer
v0.0.4
Published
WebAuthn/Passkey signer for React Native compatible with viem account abstraction
Maintainers
Readme
@mentaproject/react-native-passkey-signer
A WebAuthn/Passkey signer for React Native, compatible with viem account abstraction. This package enables the use of native passkeys (Face ID, Touch ID, fingerprint) as cryptographic authentication methods for blockchain applications.
Table of Contents
Features
- Native biometric authentication: Uses Face ID, Touch ID (iOS) or fingerprint (Android)
- viem compatible: Integrates directly with viem account abstraction for smart contract wallets
- P-256 cryptography: Uses secp256r1 elliptic curve (ECDSA with SHA-256)
- Cross-platform: Supports iOS 15+ and Android API 28+
- WebAuthn standard: Implements FIDO2/WebAuthn standard
Prerequisites
| Dependency | Minimum Version | |------------|-----------------| | react-native-passkey | >= 3.0.0 | | viem | >= 2.0.0 | | iOS | 15.0+ | | Android | API 28+ |
Installation
npm install @mentaproject/react-native-passkey-signer
# or
yarn add @mentaproject/react-native-passkey-signerAlso install the peer dependencies:
npm install react-native-passkey viemFor iOS, install the pods:
cd ios && pod installPlatform Configuration
iOS
Enable Associated Domains in Xcode:
- Open your project in Xcode
- Select your target > Signing & Capabilities
- Add "Associated Domains"
- Add
webcredentials:your-domain.com
Configure the apple-app-site-association file on your server:
{
"webcredentials": {
"apps": ["TEAM_ID.com.your.bundle.id"]
}
}This file must be accessible at https://your-domain.com/.well-known/apple-app-site-association.
Android
- Configure Digital Asset Links by creating the
assetlinks.jsonfile:
[{
"relation": ["delegate_permission/common.handle_all_urls", "delegate_permission/common.get_login_creds"],
"target": {
"namespace": "android_app",
"package_name": "com.your.package",
"sha256_cert_fingerprints": ["SHA256:XX:XX:XX:..."]
}
}]This file must be accessible at https://your-domain.com/.well-known/assetlinks.json.
- To get your SHA256 fingerprint:
keytool -list -v -keystore your-keystore.jks -alias your-aliasAPI Reference
PasskeySigner
The main class for creating and managing passkeys.
PasskeySigner.register(params)
Creates a new passkey and returns a PasskeySigner instance.
Parameters:
| Parameter | Type | Description |
|-----------|------|-------------|
| params.username | string | User identifier displayed during authentication |
| params.config | IPasskeyConfig | Passkey configuration |
Returns: Promise<PasskeySigner>
How it works:
- Generates random userId and challenge
- Calls native
Passkey.create()API with WebAuthn options - Receives CBOR-encoded attestation
- Decodes CBOR and extracts P-256 public key
- Returns new instance with credentials
const signer = await PasskeySigner.register({
username: "[email protected]",
config: {
rpId: "example.com",
rpName: "My Application",
userVerification: "required",
timeout: 60000
}
});signer.getCredential()
Returns the stored credential information.
Returns: IPasskeyCredential
const credential = signer.getCredential();
// {
// id: "abc123...",
// publicKey: { x: 123456n, y: 789012n },
// rpId: "example.com"
// }signer.getPublicKeyHex()
Returns the public key in uncompressed hexadecimal format.
Returns: Hex (format 0x04 + X coordinate (32 bytes) + Y coordinate (32 bytes))
How it works:
- Converts BigInt coordinates to hexadecimal strings
- Pads coordinates to 64 characters (32 bytes each)
- Prefixes with
0x04(uncompressed key marker)
const publicKey = signer.getPublicKeyHex();
// "0x04a1b2c3d4...6789abcd" (65 bytes = 130 hex chars + 0x prefix)signer.sign(challenge)
Signs a challenge using the passkey. Triggers biometric authentication.
Parameters:
| Parameter | Type | Description |
|-----------|------|-------------|
| challenge | Hex | Challenge to sign (hexadecimal format with 0x prefix) |
Returns:
{
authenticatorData: Uint8Array; // Authenticator data (includes rpIdHash, flags, counter)
clientDataJSON: string; // JSON containing challenge, origin, type
signature: Uint8Array; // ECDSA signature (DER format)
}How it works:
- Converts hex challenge to bytes
- Encodes to base64url for native API
- Calls
Passkey.get()which triggers biometric prompt - Receives and decodes authenticator response
- Returns raw data for further processing
const challenge = "0x1234567890abcdef...";
const { authenticatorData, clientDataJSON, signature } = await signer.sign(challenge);signer.toWebAuthnAccount()
Creates a WebAuthn account compatible with viem account abstraction.
Returns: WebAuthnAccount
How it works:
- Creates a custom
getFnfunction that intercepts WebAuthn requests - This function converts challenges and calls the native authenticator
- Formats response according to WebAuthn standard (ArrayBuffer)
- Uses viem's
toWebAuthnAccountto create the account
const webAuthnAccount = signer.toWebAuthnAccount();
// Usable with permissionless.js, ZeroDev, etc.
import { createSmartAccountClient } from "permissionless";
const smartAccountClient = createSmartAccountClient({
account: webAuthnAccount,
// ... other options
});Types
IPasskeyConfig
Configuration for WebAuthn operations.
interface IPasskeyConfig {
/** Relying Party ID (usually the domain) */
rpId: string;
/** Relying Party name displayed to user */
rpName: string;
/** Timeout in milliseconds (default: 60000) */
timeout?: number;
/**
* User verification requirement
* - "required": Always require biometrics
* - "preferred": Prefer biometrics if available
* - "discouraged": Don't ask if possible
*/
userVerification?: "required" | "preferred" | "discouraged";
}IPasskeySignerParams
Parameters for creating a PasskeySigner instance.
interface IPasskeySignerParams {
/** Username for the Passkey account */
username: string;
/** Passkey configuration */
config: IPasskeyConfig;
}IPasskeyCredential
Represents a stored passkey credential.
interface IPasskeyCredential {
/** Unique credential ID (base64url) */
id: string;
/** P-256 public key */
publicKey: {
x: bigint; // X coordinate
y: bigint; // Y coordinate
};
/** Associated Relying Party ID */
rpId: string;
}Utilities
The package also exports utility functions:
b64ToBytes(str)
Converts a base64url string to Uint8Array.
const bytes = b64ToBytes("SGVsbG8");
// Uint8Array([72, 101, 108, 108, 111])bytesToB64(bytes)
Converts a Uint8Array to base64url string (without padding).
const b64 = bytesToB64(new Uint8Array([72, 101, 108, 108, 111]));
// "SGVsbG8"extractPublicKey(authData)
Extracts X and Y coordinates of the public key from authenticator data.
const { x, y } = extractPublicKey(authData);
// x: Uint8Array (32 bytes)
// y: Uint8Array (32 bytes)bytesToBigInt(bytes)
Converts a Uint8Array to BigInt.
const num = bytesToBigInt(new Uint8Array([0x01, 0x02]));
// 258nInternal Workings
Cryptographic Algorithm
- Algorithm: ECDSA with SHA-256 (ES256, COSE identifier: -7)
- Curve: P-256 (secp256r1/prime256v1)
- Key format: Uncompressed point (65 bytes:
0x04+ X + Y)
Registration Flow
┌─────────────┐ ┌──────────────┐ ┌─────────────────┐
│ Application │────▶│ PasskeySigner │────▶│ Native Passkey │
│ │ │ .register() │ │ Module │
└─────────────┘ └──────────────┘ └─────────────────┘
│ │
│ ▼
│ ┌─────────────────┐
│ │ Biometrics │
│ │ (Face ID, etc) │
│ └─────────────────┘
│ │
▼ ▼
┌──────────────┐ ┌─────────────────┐
│ CBOR Decode │◀────│ Attestation │
│ Extract │ │ Object (CBOR) │
│ public key │ └─────────────────┘
└──────────────┘Signing Flow
┌─────────────┐ ┌──────────────┐ ┌─────────────────┐
│ Challenge │────▶│ PasskeySigner │────▶│ Native Passkey │
│ (Hex) │ │ .sign() │ │ Module │
└─────────────┘ └──────────────┘ └─────────────────┘
│ │
│ ▼
│ ┌─────────────────┐
│ │ Biometric │
│ │ Verification │
│ └─────────────────┘
│ │
▼ ▼
┌──────────────┐ ┌─────────────────┐
│ Formatted │◀────│ Signature │
│ Response │ │ ECDSA (DER) │
└──────────────┘ └─────────────────┘WebAuthn Data Structure
authenticatorData (minimum 37 bytes):
- Bytes 0-31: SHA-256 of rpId
- Byte 32: Flags (UP, UV, AT, ED)
- Bytes 33-36: Signature counter (big-endian)
- Remaining: Attested credential data (if present)
clientDataJSON:
{
"type": "webauthn.get",
"challenge": "<base64url>",
"origin": "https://example.com",
"crossOrigin": false
}Usage Examples
Registration and Storage
import { PasskeySigner } from "@mentaproject/react-native-passkey-signer";
import AsyncStorage from "@react-native-async-storage/async-storage";
async function registerUser(email: string) {
const signer = await PasskeySigner.register({
username: email,
config: {
rpId: "myapp.com",
rpName: "My Application",
userVerification: "required"
}
});
// Store credential for later use
const credential = signer.getCredential();
await AsyncStorage.setItem("passkey_credential", JSON.stringify({
id: credential.id,
publicKey: {
x: credential.publicKey.x.toString(),
y: credential.publicKey.y.toString()
},
rpId: credential.rpId
}));
return signer;
}Restoring an Existing Signer
import { PasskeySigner } from "@mentaproject/react-native-passkey-signer";
import AsyncStorage from "@react-native-async-storage/async-storage";
async function restoreSigner(): Promise<PasskeySigner | null> {
const stored = await AsyncStorage.getItem("passkey_credential");
if (!stored) return null;
const data = JSON.parse(stored);
// Recreate signer with stored credential
// Note: This requires access to the constructor via a factory method
// or storing the config as well
return signer;
}Signing a Message
async function signMessage(signer: PasskeySigner, message: string) {
// Hash the message (example with viem)
const { keccak256, toHex, toBytes } = await import("viem");
const messageHash = keccak256(toBytes(message));
const { signature, authenticatorData, clientDataJSON } = await signer.sign(messageHash);
return {
signature,
authenticatorData,
clientDataJSON,
messageHash
};
}Integration with Smart Account (ERC-4337)
import { PasskeySigner } from "@mentaproject/react-native-passkey-signer";
import { createSmartAccountClient } from "permissionless";
import { http } from "viem";
import { sepolia } from "viem/chains";
async function createSmartWallet(signer: PasskeySigner) {
const webAuthnAccount = signer.toWebAuthnAccount();
// Use with a provider like ZeroDev, Pimlico, etc.
const smartAccountClient = await createSmartAccountClient({
account: webAuthnAccount,
chain: sepolia,
transport: http("https://sepolia.infura.io/v3/YOUR_KEY"),
// ... bundler and paymaster configuration
});
return smartAccountClient;
}Sending a Transaction
async function sendTransaction(smartAccountClient: any, to: string, value: bigint) {
// Signature will be automatically requested via biometric prompt
const txHash = await smartAccountClient.sendTransaction({
to,
value,
data: "0x"
});
return txHash;
}Error Handling
import { PasskeySigner } from "@mentaproject/react-native-passkey-signer";
async function safeRegister(username: string, config: IPasskeyConfig) {
try {
const signer = await PasskeySigner.register({ username, config });
return { success: true, signer };
} catch (error) {
if (error.message.includes("UserCancelled")) {
return { success: false, error: "Authentication cancelled by user" };
}
if (error.message.includes("NotSupported")) {
return { success: false, error: "Passkeys not supported on this device" };
}
if (error.message.includes("SecurityError")) {
return { success: false, error: "Security error - check domain configuration" };
}
return { success: false, error: error.message };
}
}License
ISC
