7.2 KiB
Smirk Wallet Integration
How Wownerogue integrates with the Smirk browser extension for wallet-based authentication.
Overview
Smirk injects a window.smirk API (similar to MetaMask's window.ethereum) that websites can use to:
- Request wallet connection (get public keys)
- Request message signatures (for authentication)
- Get wallet addresses (for payouts)
Wownerogue uses the WOW (Wownero) key for authentication via a challenge-response signature flow.
Authentication Flow
Client Server Extension
| | |
|-- POST /challenge --------->| |
|<-------- { challenge } -----| |
| | |
|-- smirk.connect() ---------------------------------->|
|<-------------- { publicKeys } -----------------------|
| | |
|-- smirk.signMessage(challenge) --------------------->|
|<-------------- { signatures[] } ---------------------|
| | |
|-- POST /verify ------------>| |
| { challenge, publicKey, | |
| signature, socketId } | |
|<------ { linked: true } ----| |
| | |
|-- smirk.getAddresses() ------------------------------>|
|<-------------- { wow: "Wo..." } ---------------------|
| | |
|-- socket: address:update -->| |
API Endpoints
POST /api/auth/smirk/challenge
Generate a challenge for signature verification.
Request:
{ "socketId": "<socket.io id>" }
Response:
{ "challenge": "<64-char hex string>", "expiresIn": 300 }
The challenge is a random 32-byte hex string stored in the database with a 5-minute TTL.
POST /api/auth/smirk/verify
Verify a wallet signature and link it to the user session.
Request:
{
"socketId": "<socket.io id>",
"challenge": "<the challenge string>",
"publicKey": "<Ed25519 public key, hex>",
"signature": "<Ed25519 signature, hex>"
}
Response (success):
{ "success": true, "linked": true, "message": "Smirk wallet linked successfully" }
Errors:
400- Missing fields, invalid/expired challenge, invalid signature, or wallet already linked to another account404- No active session for the given socketId
GET /api/auth/smirk/status
Check if a session has a linked wallet.
Query: ?socketId=<socket.io id>
Response:
{ "linked": true, "hasPayoutAddress": true }
Signature Verification
The Smirk extension signs messages using Ed25519 with a SHA256 prehash:
- Extension receives the challenge string (e.g.
"a1b2c3...") - Extension computes
msgHash = SHA256(UTF8_encode(challenge)) - Extension signs
msgHashwith the Ed25519 private spend key - Extension returns the 64-byte signature as hex
The server must verify against the same prehash:
const crypto = require('crypto');
const nacl = require('tweetnacl');
const challengeHash = crypto.createHash('sha256').update(challenge).digest();
const signatureBytes = Buffer.from(signature, 'hex');
const publicKeyBytes = Buffer.from(publicKey, 'hex');
const valid = nacl.sign.detached.verify(challengeHash, signatureBytes, publicKeyBytes);
Important: Do NOT verify against Buffer.from(challenge, 'hex') (the raw hex-decoded bytes). The extension always SHA256-hashes the message string before signing.
window.smirk API Reference
The extension injects a frozen window.smirk object on all pages.
Detection
if (typeof window.smirk !== 'undefined') {
// Extension is installed
}
smirk.connect() -> Promise<PublicKeys>
Requests user approval to share public keys. Opens an approval popup on first use; subsequent calls from an approved origin return keys immediately.
const keys = await window.smirk.connect();
// keys.wow - Ed25519 public spend key (hex)
// keys.btc - secp256k1 compressed public key (hex)
// keys.ltc - secp256k1 compressed public key (hex)
// keys.xmr - Ed25519 public spend key (hex)
// keys.grin - Ed25519 public key (hex)
smirk.signMessage(message) -> Promise<SignResult>
Signs a message with all wallet keys. Always prompts for user approval.
const result = await window.smirk.signMessage(challenge);
const wowSig = result.signatures.find(s => s.asset === 'wow');
// wowSig.signature - 64-byte Ed25519 signature (hex)
// wowSig.publicKey - public key that signed (hex)
Message limit: 10,000 characters.
smirk.getAddresses() -> Promise<Addresses>
Returns wallet addresses for all assets. Requires prior connect().
const addrs = await window.smirk.getAddresses();
// addrs.wow - "Wo..." (97-char CryptoNote standard address)
// addrs.btc - "bc1q..." (bech32 P2WPKH)
// addrs.ltc - "ltc1q..." (bech32 P2WPKH)
// addrs.xmr - "4..." (95-char CryptoNote standard address)
// addrs.grin - "grin1..." (bech32 slatepack address)
smirk.isConnected() -> Promise<boolean>
Check if the current origin is connected (approved).
smirk.disconnect() -> Promise<void>
Revoke site access.
smirk.getPublicKeys() -> Promise<PublicKeys | null>
Get public keys without prompting (only works if already connected). Returns null if not connected.
Client-Side Implementation
See html/js/network/smirkAuth.js for the full implementation. Key points:
- The
SmirkAuthmodule is initialized after DOM ready and socket connection - It checks
SocketHandlers._smirkEnabled(set by server'sgame_mode_infoevent) to gate the feature - On successful auth, it calls
smirk.getAddresses()to auto-set the payout address viasocket.emit('address:update') - The button shows install link if extension is not detected, connect button if it is
Database
The smirk_challenges table stores pending challenges:
| Column | Type | Description |
|---|---|---|
| id | serial | Primary key |
| challenge | text | 64-char hex challenge string |
| socket_id | text | Socket.io session ID |
| used | boolean | Whether challenge has been consumed |
| expires_at | timestamp | Auto-set to NOW() + 5 minutes |
| created_at | timestamp | Row creation time |
The users table has a smirk_public_key column that stores the linked Ed25519 public key (hex).
Signature Schemes by Asset
| Asset | Curve | Hash | Format |
|---|---|---|---|
| BTC | secp256k1 (ECDSA) | Double SHA256 with Bitcoin message prefix | Compact signature (64 bytes) |
| LTC | secp256k1 (ECDSA) | Double SHA256 with Bitcoin message prefix | Compact signature (64 bytes) |
| XMR | Ed25519 | SHA256 prehash | R || s (64 bytes) |
| WOW | Ed25519 | SHA256 prehash | R || s (64 bytes) |
| GRIN | Ed25519 | SHA256 prehash | R || s (64 bytes) |
Wownerogue only uses the WOW signature for authentication.