mirror of
https://github.com/Such-Software/wownerogue.git
synced 2026-10-17 22:31:26 +02:00
fix smirk login
This commit is contained in:
@@ -0,0 +1,229 @@
|
||||
# Smirk Wallet Integration
|
||||
|
||||
How Wownerogue integrates with the [Smirk](https://smirk.cash) browser extension for wallet-based authentication.
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
Smirk injects a `window.smirk` API (similar to MetaMask's `window.ethereum`) that websites can use to:
|
||||
|
||||
1. Request wallet connection (get public keys)
|
||||
2. Request message signatures (for authentication)
|
||||
3. 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:**
|
||||
```json
|
||||
{ "socketId": "<socket.io id>" }
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{ "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:**
|
||||
```json
|
||||
{
|
||||
"socketId": "<socket.io id>",
|
||||
"challenge": "<the challenge string>",
|
||||
"publicKey": "<Ed25519 public key, hex>",
|
||||
"signature": "<Ed25519 signature, hex>"
|
||||
}
|
||||
```
|
||||
|
||||
**Response (success):**
|
||||
```json
|
||||
{ "success": true, "linked": true, "message": "Smirk wallet linked successfully" }
|
||||
```
|
||||
|
||||
**Errors:**
|
||||
- `400` - Missing fields, invalid/expired challenge, invalid signature, or wallet already linked to another account
|
||||
- `404` - 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:**
|
||||
```json
|
||||
{ "linked": true, "hasPayoutAddress": true }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Signature Verification
|
||||
|
||||
The Smirk extension signs messages using Ed25519 with a **SHA256 prehash**:
|
||||
|
||||
1. Extension receives the challenge string (e.g. `"a1b2c3..."`)
|
||||
2. Extension computes `msgHash = SHA256(UTF8_encode(challenge))`
|
||||
3. Extension signs `msgHash` with the Ed25519 private spend key
|
||||
4. Extension returns the 64-byte signature as hex
|
||||
|
||||
The server must verify against the same prehash:
|
||||
|
||||
```javascript
|
||||
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
|
||||
|
||||
```javascript
|
||||
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.
|
||||
|
||||
```javascript
|
||||
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.
|
||||
|
||||
```javascript
|
||||
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()`.
|
||||
|
||||
```javascript
|
||||
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 `SmirkAuth` module is initialized after DOM ready and socket connection
|
||||
- It checks `SocketHandlers._smirkEnabled` (set by server's `game_mode_info` event) to gate the feature
|
||||
- On successful auth, it calls `smirk.getAddresses()` to auto-set the payout address via `socket.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.
|
||||
+4
-2
@@ -520,12 +520,14 @@ app.post('/api/auth/smirk/verify', asyncHandler(async (req, res) => {
|
||||
let isValidSignature = false;
|
||||
try {
|
||||
const nacl = require('tweetnacl');
|
||||
const challengeBytes = Buffer.from(challenge, 'hex');
|
||||
// The Smirk extension signs SHA256(utf8(challenge)), not the raw challenge bytes.
|
||||
// We must hash the challenge the same way before verifying.
|
||||
const challengeHash = crypto.createHash('sha256').update(challenge).digest();
|
||||
const signatureBytes = Buffer.from(signature, 'hex');
|
||||
const publicKeyBytes = Buffer.from(publicKey, 'hex');
|
||||
|
||||
// tweetnacl.sign.detached.verify(message, signature, publicKey)
|
||||
isValidSignature = nacl.sign.detached.verify(challengeBytes, signatureBytes, publicKeyBytes);
|
||||
isValidSignature = nacl.sign.detached.verify(challengeHash, signatureBytes, publicKeyBytes);
|
||||
} catch (verifyError) {
|
||||
console.error('Signature verification error:', verifyError.message);
|
||||
throw new ValidationError('Signature verification failed', {
|
||||
|
||||
Reference in New Issue
Block a user