A WebAuthn/Passkey authentication server designed for secure, passwordless authentication with support for encrypted data storage. This server is specifically architected to work as a secure authentication layer for applications that need to protect sensitive data like cryptocurrency wallet seeds.
This server implements a security architecture where:
- Passkeys serve as the authentication mechanism (the "lock")
- AES encryption keys are stored server-side, protected by passkey authentication
- Encrypted sensitive data (e.g., Bitcoin seed phrases) are stored in client-side secure storage (iCloud Keychain)
- The server never has access to decrypt user's sensitive data
User → Passkey Authentication → Server returns AES Key → Decrypt local encrypted data
This separation ensures that:
- Server compromise alone cannot decrypt user data
- Client-side breach alone cannot decrypt without authentication
- Users can safely backup encrypted data anywhere (QR codes, cloud storage, etc.)
- Key Features
- Prerequisites
- Server Setup
- API Documentation
- iOS/Swift Integration Guide
- Wallet Implementation Flow
- Testing
- Security Considerations
- Passkey Authentication: WebAuthn-based passwordless authentication
- Secure Key Storage: AES encryption keys stored server-side, accessible only after authentication
- Anonymous User Support: Users can use the service without providing personal information
- Multi-Passkey Support: Users can register multiple passkeys for account recovery
- Encrypted Data Storage: Store encrypted blobs (like AES keys) tied to user accounts
- Dashboard: Web interface for monitoring users and passkeys
- Cross-Platform: Works with iOS, Android, and web clients
- Node.js 20+
- PostgreSQL
- Port 3000 available
- Xcode 14+
- iOS 16+ device or simulator
- Swift 5.0+
- Understanding of ASAuthorization framework
The server is already running on this machine at http://localhost:3000
- API:
http://localhost:3000 - Dashboard:
http://localhost:3000/dashboard - Health Check:
http://localhost:3000/health
RP_ID=localhost
RP_NAME=Nuri Passkey Server
ORIGIN=https://localhost-
Registration Flow:
- GET
/generate-registration-options→ Get WebAuthn options - User completes biometric/passkey creation on device
- POST
/verify-registration→ Verify and store credential
- GET
-
Login Flow:
- GET
/generate-authentication-options→ Get challenge - User completes biometric/passkey verification
- POST
/verify-authentication→ Verify and authenticate
- GET
GET /generate-registration-options?username=john_doeQuery Parameters:
username(optional): For named users. Omit for anonymous users.
Response:
{
"challenge": "base64url-encoded-challenge",
"rp": {
"name": "Nuri Passkey Server",
"id": "localhost"
},
"user": {
"id": "base64url-encoded-user-id",
"name": "john_doe",
"displayName": "john_doe"
},
"pubKeyCredParams": [...],
"timeout": 60000,
"attestation": "none",
"excludeCredentials": [],
"authenticatorSelection": {
"residentKey": "required",
"userVerification": "required"
},
"challengeKey": "john_doe"
}POST /verify-registration
Content-Type: application/json
{
"username": "john_doe",
"challengeKey": "john_doe",
"cred": {
"id": "credential-id",
"rawId": "base64url-encoded-raw-id",
"type": "public-key",
"response": {
"attestationObject": "base64url-encoded",
"clientDataJSON": "base64url-encoded",
"transports": ["internal", "hybrid"]
}
}
}Response:
{
"verified": true,
"username": "john_doe",
"isAnonymous": false
}GET /generate-authentication-optionsResponse:
{
"challenge": "base64url-encoded-challenge",
"timeout": 60000,
"rpId": "localhost",
"allowCredentials": [],
"userVerification": "required"
}POST /verify-authentication
Content-Type: application/json
{
"cred": {
"id": "credential-id",
"rawId": "base64url-encoded-raw-id",
"type": "public-key",
"response": {
"authenticatorData": "base64url-encoded",
"clientDataJSON": "base64url-encoded",
"signature": "base64url-encoded",
"userHandle": "base64url-encoded"
}
}
}Response:
{
"verified": true,
"username": "john_doe",
"isAnonymous": false
}POST /api/users/:username/data
Content-Type: application/json
{
"encryptedData": {
"aesKey": "base64-encoded-aes-key"
},
"credentialId": "credential-id-for-anonymous"
}Note: This endpoint requires prior authentication. The AES key should be generated client-side and stored here for later retrieval.
GET /api/users/:username/data?credentialId=xxxReturns:
{
"username": "john_doe",
"encryptedData": {
"aesKey": "base64-encoded-aes-key"
}
}GET /dashboard- Web dashboardGET /api/dashboard- Dashboard API dataDELETE /api/users/:username- Delete userDELETE /api/clear-database- Clear all data (dev only)
The integration follows this flow:
- User taps "Passkey" button
- App attempts authentication
- If passkey exists → Retrieve AES key → Decrypt local data
- If no passkey → Create one → Generate AES key → Store on server
AuthenticationServicesfor passkey operationsCryptoKitfor AES encryption- Standard networking libraries for API calls
-
Single Button Flow
- Call
/generate-authentication-options - Present ASAuthorization UI
- Handle success/failure appropriately
- Call
-
Data Encoding
- All binary data must be base64url encoded
- Challenges come as base64url, decode for iOS use
- Encode responses before sending to server
-
Key Storage Architecture
- Generate AES-256 key on first use
- Store AES key on server via
/api/users/{username}/data - Store encrypted Bitcoin seed in iCloud Keychain
- Never store AES key and encrypted data together
- User opens app → Shows welcome screen with single "Passkey" button
- Tap Passkey → Call
/generate-authentication-options - No passkey found → System shows "Create Passkey" option
- Create passkey → Call
/generate-registration-optionsand/verify-registration - Generate AES key → Create random 256-bit key client-side
- Store AES key → POST to
/api/users/{username}/data - Encrypt seed → Use AES key to encrypt Bitcoin seed
- Store encrypted seed → Save to iCloud Keychain
- Success → Navigate to main wallet screen
- User opens app → Shows welcome screen
- Tap Passkey → Call
/generate-authentication-options - Select passkey → iOS shows available passkeys
- Authenticate → Call
/verify-authentication - Fetch AES key → GET from
/api/users/{username}/data - Retrieve encrypted seed → From iCloud Keychain
- Decrypt seed → Use AES key in memory
- Success → Navigate to main wallet screen
- From iCloud: Authenticate → Get AES key from server → Decrypt
- From QR Code: Scan encrypted seed → Authenticate → Get AES key → Decrypt
- New Device: Sign in with Apple ID → Passkey syncs → Normal flow
┌─────────────┐ ┌──────────────┐ ┌────────────────┐
│ Passkey │────▶│ Server │────▶│ AES Key │
│ (Lock) │ │ (Stores) │ │ (Plain text) │
└─────────────┘ └──────────────┘ └────────────────┘
│
▼
┌─────────────┐ ┌──────────────┐ ┌────────────────┐
│ iCloud │────▶│ Encrypted │◀────│ Bitcoin │
│ Keychain │ │ Seed │ │ Seed │
└─────────────┘ └──────────────┘ └────────────────┘
Key Points:
- Passkey authenticates user
- Server returns AES key (only after auth)
- AES key decrypts seed stored in iCloud
- Server never sees Bitcoin seed
- Encrypted seed can be safely backed up anywhere
- Server running at:
http://localhost:3000 - For iOS Simulator: Use
http://localhost:3000 - For physical device: Use machine's IP address (e.g.,
http://192.168.1.x:3000)
# Health check
curl http://localhost:3000/health
# View dashboard
open http://localhost:3000/dashboard- Separation of Concerns: AES key and encrypted data stored separately
- Zero-Knowledge: Server cannot decrypt user's Bitcoin seeds
- Multi-Factor: Requires both passkey auth and access to encrypted data
- Backup Friendly: Encrypted seeds can be stored anywhere safely
- Never store AES key and encrypted seed in the same location
- Always generate AES keys using cryptographically secure methods
- Clear memory after using sensitive data
- Use AES-256-GCM for authenticated encryption
- Implement rate limiting for production use
- Server breach: Attacker only gets AES keys, no encrypted data
- iCloud breach: Attacker only gets encrypted data, no keys
- Lost device: Passkey + iCloud sync enables recovery
- Phishing: Passkeys are domain-bound and cannot be phished
- Use HTTPS with valid certificates
- Implement proper session management
- Add rate limiting and DDoS protection
- Regular security audits
- Secure database backups
- Monitor for suspicious authentication patterns
See DEPLOYMENT.md for detailed production deployment instructions.
- Clone this repository
- Copy
.env.exampleto.envand configure - Install dependencies:
npm install - Run locally:
npm start
- Deployed to: Hetzner Cloud (CPX11)
- Live at: https://passkey.nuri.com
- Architecture: Node.js + PostgreSQL + Nginx
- SSL: Let's Encrypt
For issues with the server:
- Check server health:
curl http://localhost:3000/health - View dashboard:
http://localhost:3000/dashboard - Production logs:
pm2 logs passkey-server