Passkey Login
Passkey Login lets users access their wallet with biometrics (fingerprint, face scan, or device PIN) instead of writing down and safeguarding a seed phrase. The SDK uses the WebAuthn PRF extension to deterministically derive wallet keys from a passkey. Keys are never stored; they're regenerated on demand each time the user authenticates. The protocol also supports multiple wallets, each derived from a different label, with labels discoverable via Nostr relays.
For the full technical specification, see the Passkey Login spec.
Application configuration
Relying Party ID
The domain keys.breez.technology serves as a common Relying Party (RP) that enables cross-app passkey sharing. Applications that use this RP ID allow users to access the same passkey credentials across different platforms and apps.
To enable this cross-domain passkey sharing, keys.breez.technology serves three configuration files that declare which origins and apps are authorized to use it as an RP ID.
Web: Related Origins
File: https://keys.breez.technology/.well-known/webauthn
Declares which web origins can use the centralized RP ID for WebAuthn operations:
{
"related_origins": [
"https://keys.breez.technology",
"https://your-app.example.com"
]
}
To register your web origin, contact us to have it added to this file.
Android: Asset Links
File: https://keys.breez.technology/.well-known/assetlinks.json
Establishes digital asset links between the domain and Android applications:
[
{
"relation": [
"delegate_permission/common.handle_all_urls",
"delegate_permission/common.get_login_creds"
],
"target": {
"namespace": "android_app",
"package_name": "com.example.yourapp",
"sha256_cert_fingerprints": [
"B6:16:AD:FE:C5:C6:D3:4C:93:01:5B:4A:79:20:21:4E:62:43:AB:29:28:EE:34:9A:F2:46:55:4B:54:FC:42:DF"
]
}
}
]
Replace com.example.yourapp with your application package name and the fingerprint with your app's signing certificate SHA256 fingerprint. See the Digital Asset Links documentation and Credential Manager prerequisites for details.
To register your Android app, contact us with the details outlined to have it added to this file.
iOS: Apple App Site Association
File: https://keys.breez.technology/.well-known/apple-app-site-association
Connects the domain to iOS applications for passkey sharing:
{
"webcredentials": {
"apps": [
"TEAMID.com.example.yourapp"
]
}
}
Replace TEAMID with your Apple Developer Team ID and com.example.yourapp with your application bundle identifier.
Your app must have the Associated Domains capability enabled. In Xcode, go to Signing & Capabilities → add Associated Domains → add the entry webcredentials:keys.breez.technology.
Expo Managed Workflow
If you're using Expo, the Breez SDK plugin can configure this automatically. See the React Native/Expo installation guide for details on theenablePasskey option.
To register your iOS app, contact us with the details outlined to have it added to this file.
Nostr relay configuration
The SDK uses Nostr relays to store and discover labels. Configure relay access by passing a NostrRelayConfigNostrRelayConfigNostrRelayConfigNostrRelayConfigNostrRelayConfigNostrRelayConfigNostrRelayConfigNostrRelayConfigNostrRelayConfig when constructing the PasskeyPasskeyPasskeyPasskeyPasskeyPasskeyPasskeyPasskeyPasskey instance:
breez_api_keybreez_api_keybreezApiKeybreezApiKeybreezApiKeybreezApiKeybreezApiKeyBreezApiKeyBreezApiKey- Your Breez API key. When provided, the SDK connects to the Breez-managed relay with NIP-42 authentication.timeout_secstimeout_secstimeoutSecstimeoutSecstimeoutSecstimeoutSecstimeoutSecsTimeoutSecsTimeoutSecs- Connection timeout in seconds (defaults to 30).
The SDK also implements NIP-65 to discover and publish to additional public relays for redundancy. See the Listing labels and Storing a label code examples below for usage.
Implementing the PRF provider
Your application must implement the PRF provider to interface with platform passkey APIs.
/// In practice, implement using platform-specific passkey APIs.
struct ExamplePasskeyPrfProvider;
#[async_trait::async_trait]
impl PasskeyPrfProvider for ExamplePasskeyPrfProvider {
async fn derive_prf_seed(&self, _salt: String) -> Result<Vec<u8>, PasskeyPrfError> {
// Call platform passkey API with PRF extension
// Returns 32-byte PRF output
todo!("Implement using WebAuthn or native passkey APIs")
}
async fn is_prf_available(&self) -> Result<bool, PasskeyPrfError> {
// Check if PRF-capable passkey exists
todo!("Check platform passkey availability")
}
}
// In practice, implement using platform-specific passkey APIs.
class ExamplePasskeyPrfProvider: PasskeyPrfProvider {
func derivePrfSeed(salt: String) async throws -> Data {
// Call platform passkey API with PRF extension
// Returns 32-byte PRF output
fatalError("Implement using WebAuthn or native passkey APIs")
}
func isPrfAvailable() async throws -> Bool {
// Check if PRF-capable passkey exists
fatalError("Check platform passkey availability")
}
}
// In practice, implement PRF provider using platform passkey APIs
class ExamplePasskeyPrfProvider : PasskeyPrfProvider {
override suspend fun derivePrfSeed(salt: String): ByteArray {
// Call platform passkey API with PRF extension
// Returns 32-byte PRF output
TODO("Implement using WebAuthn or native passkey APIs")
}
override suspend fun isPrfAvailable(): Boolean {
// Check if PRF-capable passkey exists
TODO("Check platform passkey availability")
}
}
// In practice, implement using platform-specific passkey APIs.
class ExamplePasskeyPrfProvider : PasskeyPrfProvider
{
public async Task<byte[]> DerivePrfSeed(string salt)
{
// Call platform passkey API with PRF extension
// Returns 32-byte PRF output
throw new NotImplementedException("Implement using WebAuthn or native passkey APIs");
}
public async Task<bool> IsPrfAvailable()
{
// Check if PRF-capable passkey exists
throw new NotImplementedException("Check platform passkey availability");
}
}
// In practice, implement PRF provider using WebAuthn API
class ExamplePasskeyPrfProvider {
derivePrfSeed = async (salt: string): Promise<Uint8Array> => {
// Call platform passkey API with PRF extension
// Returns 32-byte PRF output
throw new Error('Implement using WebAuthn or native passkey APIs')
}
isPrfAvailable = async (): Promise<boolean> => {
// Check if PRF-capable passkey exists
throw new Error('Check platform passkey availability')
}
}
// In practice, implement PRF provider using platform passkey APIs
class ExamplePasskeyPrfProvider {
derivePrfSeed = async (salt: string): Promise<ArrayBuffer> => {
// Call platform passkey API with PRF extension
// Returns 32-byte PRF output
throw new Error('Implement using WebAuthn or native passkey APIs')
}
isPrfAvailable = async (): Promise<boolean> => {
// Check if PRF-capable passkey exists
throw new Error('Check platform passkey availability')
}
}
// Implement these functions using platform passkey APIs.
Future<Uint8List> derivePrfSeed(String salt) async {
// Call platform passkey API with PRF extension
// Returns 32-byte PRF output
throw UnimplementedError('Implement using platform passkey APIs');
}
Future<bool> isPrfAvailable() async {
// Check if PRF-capable passkey exists
throw UnimplementedError('Check platform passkey availability');
}
# In practice, implement using platform-specific passkey APIs.
class ExamplePasskeyPrfProvider(PasskeyPrfProvider):
async def derive_prf_seed(self, salt: str):
# Call platform passkey API with PRF extension
# Returns 32-byte PRF output
raise NotImplementedError("Implement using WebAuthn or native passkey APIs")
async def is_prf_available(self):
# Check if PRF-capable passkey exists
raise NotImplementedError("Check platform passkey availability")
// In practice, implement using platform-specific passkey APIs.
type ExamplePasskeyPrfProvider struct{}
func (p *ExamplePasskeyPrfProvider) DerivePrfSeed(salt string) ([]byte, error) {
// Call platform passkey API with PRF extension
// Returns 32-byte PRF output
panic("Implement using WebAuthn or native passkey APIs")
}
func (p *ExamplePasskeyPrfProvider) IsPrfAvailable() (bool, error) {
// Check if PRF-capable passkey exists
panic("Check platform passkey availability")
}
Platform considerations
-
Web (browsers): Use the WebAuthn API with the
prfextension. Browsers handle the salt transformation internally. Use discoverable credentials (residentKey: 'required') with emptyallowCredentialsfor assertion so the browser discovers the credential by RP ID. -
Android / iOS: Use native passkey APIs with PRF support. Ensure the Associated Domains / Asset Links configuration is in place for
keys.breez.technology. -
CLI / Desktop (CTAP2): Use the
hmac-secretextension directly. Non-browser implementations must apply the WebAuthn salt transformation manually to produce the same PRF output as browsers:actualSalt = SHA-256("WebAuthn PRF" || 0x00 || developerSalt)This transformation is defined in the W3C WebAuthn PRF extension spec and ensures that the same passkey + salt produces identical seeds across browser and native implementations.
Connecting with a passkey API docs
To connect with a passkey, call Passkey.get_walletPasskey.get_walletPasskey.getWalletPasskey.getWalletPasskey.getWalletPasskey.getWalletPasskey.getWalletPasskey.GetWalletPasskey.GetWallet to derive a wallet, then pass its seed to connectconnectconnectconnectconnectconnectconnectConnectConnect. The label defaults to "Default" when omitted.
let prf_provider = Arc::new(ExamplePasskeyPrfProvider);
let passkey = Passkey::new(prf_provider, None);
// Derive the wallet from the passkey (pass None for the default wallet)
let wallet = passkey.get_wallet(Some("personal".to_string())).await?;
let config = default_config(Network::Mainnet);
let sdk = connect(ConnectRequest {
config,
seed: wallet.seed,
storage_dir: "./.data".to_string(),
})
.await?;
let prfProvider = ExamplePasskeyPrfProvider()
let passkey = Passkey(prfProvider: prfProvider, relayConfig: nil)
// Derive the wallet from the passkey (pass nil for the default wallet)
let wallet = try await passkey.getWallet(label: "personal")
let config = defaultConfig(network: .mainnet)
let sdk = try await connect(
request: ConnectRequest(
config: config,
seed: wallet.seed,
storageDir: "./.data"
))
val prfProvider = ExamplePasskeyPrfProvider()
val passkey = Passkey(prfProvider, null)
// Derive the wallet from the passkey (pass null for the default wallet)
val wallet = passkey.getWallet("personal")
val config = defaultConfig(Network.MAINNET)
val sdk = connect(ConnectRequest(config, wallet.seed, "./.data"))
var prfProvider = new ExamplePasskeyPrfProvider();
var passkey = new Passkey(prfProvider, null);
// Derive the wallet from the passkey (pass null for the default wallet)
var wallet = await passkey.GetWallet(label: "personal");
var config = BreezSdkSparkMethods.DefaultConfig(network: Network.Mainnet);
var sdk = await BreezSdkSparkMethods.Connect(new ConnectRequest(
config: config,
seed: wallet.seed,
storageDir: "./.data"
));
const prfProvider = new ExamplePasskeyPrfProvider()
const passkey = new Passkey(prfProvider, undefined)
// Construct the wallet using the passkey (pass undefined for the default wallet)
const wallet = await passkey.getWallet('personal')
const config = defaultConfig('mainnet')
const sdk = await connect({ config, seed: wallet.seed, storageDir: './.data' })
const prfProvider = new ExamplePasskeyPrfProvider()
const passkey = new Passkey(prfProvider, undefined)
// Construct the wallet using the passkey (pass undefined for the default wallet)
const wallet = await passkey.getWallet('personal')
const config = defaultConfig(Network.Mainnet)
const sdk = await connect({ config, seed: wallet.seed, storageDir: './.data' })
final passkey = Passkey(
derivePrfSeed: derivePrfSeed,
isPrfAvailable: isPrfAvailable,
);
// Derive the wallet from the passkey (pass null for the default wallet)
final wallet = await passkey.getWallet(label: "personal");
final config = defaultConfig(network: Network.mainnet);
final sdk = await connect(
request: ConnectRequest(
config: config, seed: wallet.seed, storageDir: "./.data"));
prf_provider = ExamplePasskeyPrfProvider()
passkey = Passkey(prf_provider, None)
# Derive the wallet from the passkey (pass None for the default wallet)
wallet = await passkey.get_wallet("personal")
config = default_config(network=Network.MAINNET)
sdk = await connect(ConnectRequest(config=config, seed=wallet.seed, storage_dir="./.data"))
prfProvider := &ExamplePasskeyPrfProvider{}
passkey := breez_sdk_spark.NewPasskey(prfProvider, nil)
// Derive the wallet from the passkey (pass nil for the default wallet)
label := "personal"
wallet, err := passkey.GetWallet(&label)
if err != nil {
return nil, err
}
config := breez_sdk_spark.DefaultConfig(breez_sdk_spark.NetworkMainnet)
sdk, err := breez_sdk_spark.Connect(breez_sdk_spark.ConnectRequest{
Config: config,
Seed: wallet.Seed,
StorageDir: "./.data",
})
if err != nil {
return nil, err
}
Listing labels API docs
Discover labels associated to the passkey using Nostr.
let prf_provider = Arc::new(ExamplePasskeyPrfProvider);
let relay_config = NostrRelayConfig {
breez_api_key: Some("<breez api key>".to_string()),
..NostrRelayConfig::default()
};
let passkey = Passkey::new(prf_provider, Some(relay_config));
// Query Nostr for labels associated with this passkey
let labels = passkey.list_labels().await?;
for label in &labels {
println!("Found label: {}", label);
}
let prfProvider = ExamplePasskeyPrfProvider()
let relayConfig = NostrRelayConfig(breezApiKey: "<breez api key>")
let passkey = Passkey(prfProvider: prfProvider, relayConfig: relayConfig)
// Query Nostr for labels associated with this passkey
let labels = try await passkey.listLabels()
for label in labels {
print("Found label: \(label)")
}
val prfProvider = ExamplePasskeyPrfProvider()
val relayConfig = NostrRelayConfig(breezApiKey = "<breez api key>")
val passkey = Passkey(prfProvider, relayConfig)
// Query Nostr for labels associated with this passkey
val labels = passkey.listLabels()
for (label in labels) {
// Log.v("Breez", "Found label: $label")
}
var prfProvider = new ExamplePasskeyPrfProvider();
var relayConfig = new NostrRelayConfig(
breezApiKey: "<breez api key>"
);
var passkey = new Passkey(prfProvider, relayConfig);
// Query Nostr for labels associated with this passkey
var labels = await passkey.ListLabels();
foreach (var label in labels)
{
Console.WriteLine($"Found label: {label}");
}
const prfProvider = new ExamplePasskeyPrfProvider()
const relayConfig: NostrRelayConfig = {
breezApiKey: '<breez api key>'
}
const passkey = new Passkey(prfProvider, relayConfig)
// Query Nostr for labels associated with this passkey
const labels = await passkey.listLabels()
for (const label of labels) {
console.log(`Found label: ${label}`)
}
const prfProvider = new ExamplePasskeyPrfProvider()
const relayConfig: NostrRelayConfig = {
breezApiKey: '<breez api key>',
timeoutSecs: undefined
}
const passkey = new Passkey(prfProvider, relayConfig)
// Query Nostr for labels associated with this passkey
const labels = await passkey.listLabels()
for (const label of labels) {
console.log(`Found label: ${label}`)
}
final relayConfig = NostrRelayConfig(
breezApiKey: '<breez api key>',
);
final passkey = Passkey(
derivePrfSeed: derivePrfSeed,
isPrfAvailable: isPrfAvailable,
relayConfig: relayConfig,
);
// Query Nostr for labels associated with this passkey
final labels = await passkey.listLabels();
for (final label in labels) {
print("Found label: $label");
}
prf_provider = ExamplePasskeyPrfProvider()
relay_config = NostrRelayConfig(breez_api_key="<breez api key>")
passkey = Passkey(prf_provider, relay_config)
# Query Nostr for labels associated with this passkey
labels = await passkey.list_labels()
for label in labels:
print(f"Found label: {label}")
prfProvider := &ExamplePasskeyPrfProvider{}
breezApiKey := "<breez api key>"
relayConfig := &breez_sdk_spark.NostrRelayConfig{
BreezApiKey: &breezApiKey,
}
passkey := breez_sdk_spark.NewPasskey(prfProvider, relayConfig)
// Query Nostr for labels associated with this passkey
labels, err := passkey.ListLabels()
if err != nil {
return nil, err
}
for _, label := range labels {
log.Printf("Found label: %s", label)
}
Storing a label API docs
Publish a label to Nostr so it can be discovered later.
let prf_provider = Arc::new(ExamplePasskeyPrfProvider);
let relay_config = NostrRelayConfig {
breez_api_key: Some("<breez api key>".to_string()),
..NostrRelayConfig::default()
};
let passkey = Passkey::new(prf_provider, Some(relay_config));
// Publish the label to Nostr for later discovery
passkey.store_label("personal".to_string()).await?;
let prfProvider = ExamplePasskeyPrfProvider()
let relayConfig = NostrRelayConfig(breezApiKey: "<breez api key>")
let passkey = Passkey(prfProvider: prfProvider, relayConfig: relayConfig)
// Publish the label to Nostr for later discovery
try await passkey.storeLabel(label: "personal")
val prfProvider = ExamplePasskeyPrfProvider()
val relayConfig = NostrRelayConfig(breezApiKey = "<breez api key>")
val passkey = Passkey(prfProvider, relayConfig)
// Publish the label to Nostr for later discovery
passkey.storeLabel("personal")
var prfProvider = new ExamplePasskeyPrfProvider();
var relayConfig = new NostrRelayConfig(
breezApiKey: "<breez api key>"
);
var passkey = new Passkey(prfProvider, relayConfig);
// Publish the label to Nostr for later discovery
await passkey.StoreLabel(label: "personal");
const prfProvider = new ExamplePasskeyPrfProvider()
const relayConfig: NostrRelayConfig = {
breezApiKey: '<breez api key>'
}
const passkey = new Passkey(prfProvider, relayConfig)
// Publish the label to Nostr for later discovery
await passkey.storeLabel('personal')
const prfProvider = new ExamplePasskeyPrfProvider()
const relayConfig: NostrRelayConfig = {
breezApiKey: '<breez api key>',
timeoutSecs: undefined
}
const passkey = new Passkey(prfProvider, relayConfig)
// Publish the label to Nostr for later discovery
await passkey.storeLabel('personal')
final relayConfig = NostrRelayConfig(
breezApiKey: '<breez api key>',
);
final passkey = Passkey(
derivePrfSeed: derivePrfSeed,
isPrfAvailable: isPrfAvailable,
relayConfig: relayConfig,
);
// Publish the label to Nostr for later discovery
await passkey.storeLabel(label: "personal");
prf_provider = ExamplePasskeyPrfProvider()
relay_config = NostrRelayConfig(breez_api_key="<breez api key>")
passkey = Passkey(prf_provider, relay_config)
# Publish the label to Nostr for later discovery
await passkey.store_label(label="personal")
prfProvider := &ExamplePasskeyPrfProvider{}
breezApiKey := "<breez api key>"
relayConfig := &breez_sdk_spark.NostrRelayConfig{
BreezApiKey: &breezApiKey,
}
passkey := breez_sdk_spark.NewPasskey(prfProvider, relayConfig)
// Publish the label to Nostr for later discovery
err := passkey.StoreLabel("personal")
if err != nil {
return err
}
Best practices
Cache the user-selected label
Store the label locally (e.g., localStorage on web, SharedPreferences on Android, UserDefaults on iOS) if selected by the user. This allows the app to skip the label selection step on subsequent launches and go straight to passkey authentication.
Never store the derived mnemonic
The mnemonic should always be re-derived from the passkey and label on each session. The passkey authentication (biometric, PIN, etc.) is the security boundary — storing the mnemonic would bypass it. On app restart, check for a cached label and prompt the user for passkey authentication to derive the seed.
Allow manual mnemonic backup
Provide a way for users to reveal their derived 12-word mnemonic as an emergency backup. This should be user-initiated (e.g., behind a "Show recovery phrase" button) and derived on-demand via Passkey.get_walletPasskey.get_walletPasskey.getWalletPasskey.getWalletPasskey.getWalletPasskey.getWalletPasskey.getWalletPasskey.GetWalletPasskey.GetWallet with the cached label. This gives users a safety net if they lose access to their passkey.
Offer a mnemonic fallback
Not all devices support the PRF extension. Check Passkey.is_availablePasskey.is_availablePasskey.isAvailablePasskey.isAvailablePasskey.isAvailablePasskey.isAvailablePasskey.isAvailablePasskey.IsAvailablePasskey.IsAvailable at startup and present the appropriate flow — seedless for capable devices, traditional mnemonic backup/restore for others.
Handle label discovery failures
When discovering labels, Passkey.list_labelsPasskey.list_labelsPasskey.listLabelsPasskey.listLabelsPasskey.listLabelsPasskey.listLabelsPasskey.listLabelsPasskey.ListLabelsPasskey.ListLabels may return an empty list if relays are unreachable or the label events have been pruned. Always allow manual label entry as a fallback alongside the Nostr-discovered list.
Supported specs
- Seedless Restore Passkey-based wallet derivation and discovery
- Nostr Relay-based event protocol for label storage
- NIP-42 Authentication of clients to relays
- NIP-65 Relay List Metadata