nostr verification working

This commit is contained in:
counterweight 2025-02-12 00:44:17 +01:00
parent f42ae5fc1d
commit 19667807bb
Signed by: counterweight
GPG key ID: 883EDBAA726BD96C
8 changed files with 258 additions and 7 deletions

119
package-lock.json generated
View file

@ -14,6 +14,7 @@
"dotenv": "^16.4.7", "dotenv": "^16.4.7",
"ejs": "^3.1.10", "ejs": "^3.1.10",
"express": "^4.17.1", "express": "^4.17.1",
"nostr-tools": "^2.10.4",
"pg": "^8.13.1", "pg": "^8.13.1",
"sequelize": "^6.37.5", "sequelize": "^6.37.5",
"sqlite3": "^5.1.7", "sqlite3": "^5.1.7",
@ -26,6 +27,47 @@
"integrity": "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==", "integrity": "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==",
"optional": true "optional": true
}, },
"node_modules/@noble/ciphers": {
"version": "0.5.3",
"resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-0.5.3.tgz",
"integrity": "sha512-B0+6IIHiqEs3BPMT0hcRmHvEj2QHOLu+uwt+tqDDeVd0oyVzh7BPrDcPjRnV1PV/5LaknXJJQvOuRGR0zQJz+w==",
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@noble/curves": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.2.0.tgz",
"integrity": "sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==",
"dependencies": {
"@noble/hashes": "1.3.2"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@noble/curves/node_modules/@noble/hashes": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.2.tgz",
"integrity": "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==",
"engines": {
"node": ">= 16"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@noble/hashes": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.1.tgz",
"integrity": "sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA==",
"engines": {
"node": ">= 16"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@npmcli/fs": { "node_modules/@npmcli/fs": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-1.1.1.tgz", "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-1.1.1.tgz",
@ -50,6 +92,53 @@
"node": ">=10" "node": ">=10"
} }
}, },
"node_modules/@scure/base": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.1.tgz",
"integrity": "sha512-ZxOhsSyxYwLJj3pLZCefNitxsj093tb2vq90mp2txoYeBqbcjDjqFhyM8eUjq/uFm6zJ+mUuqxlS2FkuSY1MTA==",
"funding": [
{
"type": "individual",
"url": "https://paulmillr.com/funding/"
}
]
},
"node_modules/@scure/bip32": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.3.1.tgz",
"integrity": "sha512-osvveYtyzdEVbt3OfwwXFr4P2iVBL5u1Q3q4ONBfDY/UpOuXmOlbgwc1xECEboY8wIays8Yt6onaWMUdUbfl0A==",
"dependencies": {
"@noble/curves": "~1.1.0",
"@noble/hashes": "~1.3.1",
"@scure/base": "~1.1.0"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@scure/bip32/node_modules/@noble/curves": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.1.0.tgz",
"integrity": "sha512-091oBExgENk/kGj3AZmtBDMpxQPDtxQABR2B9lb1JbVTs6ytdzZNwvhxQ4MWasRNEzlbEH8jCWFCwhF/Obj5AA==",
"dependencies": {
"@noble/hashes": "1.3.1"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@scure/bip39": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.2.1.tgz",
"integrity": "sha512-Z3/Fsz1yr904dduJD0NpiyRHhRYHdcnyh73FZWiV+/qhWi83wNJ3NWolYqCEN+ZWsUz2TWwajJggcRE9r1zUYg==",
"dependencies": {
"@noble/hashes": "~1.3.0",
"@scure/base": "~1.1.0"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@tootallnate/once": { "node_modules/@tootallnate/once": {
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz",
@ -1577,6 +1666,36 @@
"node": ">=6" "node": ">=6"
} }
}, },
"node_modules/nostr-tools": {
"version": "2.10.4",
"resolved": "https://registry.npmjs.org/nostr-tools/-/nostr-tools-2.10.4.tgz",
"integrity": "sha512-biU7sk+jxHgVASfobg2T5ttxOGGSt69wEVBC51sHHOEaKAAdzHBLV/I2l9Rf61UzClhliZwNouYhqIso4a3HYg==",
"dependencies": {
"@noble/ciphers": "^0.5.1",
"@noble/curves": "1.2.0",
"@noble/hashes": "1.3.1",
"@scure/base": "1.1.1",
"@scure/bip32": "1.3.1",
"@scure/bip39": "1.2.1"
},
"optionalDependencies": {
"nostr-wasm": "0.1.0"
},
"peerDependencies": {
"typescript": ">=5.0.0"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
}
},
"node_modules/nostr-wasm": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/nostr-wasm/-/nostr-wasm-0.1.0.tgz",
"integrity": "sha512-78BTryCLcLYv96ONU8Ws3Q1JzjlAt+43pWQhIl86xZmWeegYCNLPml7yQ+gG3vR6V5h4XGj+TxO+SS5dsThQIA==",
"optional": true
},
"node_modules/npmlog": { "node_modules/npmlog": {
"version": "6.0.2", "version": "6.0.2",
"resolved": "https://registry.npmjs.org/npmlog/-/npmlog-6.0.2.tgz", "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-6.0.2.tgz",

View file

@ -7,6 +7,7 @@
"dotenv": "^16.4.7", "dotenv": "^16.4.7",
"ejs": "^3.1.10", "ejs": "^3.1.10",
"express": "^4.17.1", "express": "^4.17.1",
"nostr-tools": "^2.10.4",
"pg": "^8.13.1", "pg": "^8.13.1",
"sequelize": "^6.37.5", "sequelize": "^6.37.5",
"sqlite3": "^5.1.7", "sqlite3": "^5.1.7",

View file

@ -1,5 +1,7 @@
const DEFAULT_SESSION_DURATION_SECONDS = 60 * 60 * 24 * 30; const DEFAULT_SESSION_DURATION_SECONDS = 60 * 60 * 24 * 30;
const DEFAULT_NOSTR_CHALLENGE_DURATION_SECONDS = 60 * 60 * 24 * 30;
module.exports = { module.exports = {
DEFAULT_SESSION_DURATION_SECONDS DEFAULT_SESSION_DURATION_SECONDS,
DEFAULT_NOSTR_CHALLENGE_DURATION_SECONDS
} }

View file

@ -0,0 +1,27 @@
const { DataTypes } = require('sequelize');
const sequelize = require('../database');
const NostrChallengeCompleted = sequelize.define('NostrChallengeCompleted', {
uuid: {
type: DataTypes.UUID,
allowNull: false,
unique: true,
primaryKey: true
},
challenge: {
type: DataTypes.STRING,
allowNull: false
},
signed_event: {
type: DataTypes.JSONB,
allowNull: false
},
created_at: {
type: DataTypes.DATE,
allowNull: false
}
}, {
tableName: 'nostr_challenge_completed'
});
module.exports = NostrChallengeCompleted;

View file

@ -12,6 +12,10 @@ const NostrChallengeCreated = sequelize.define('NostrChallengeCreated', {
type: DataTypes.STRING, type: DataTypes.STRING,
allowNull: false allowNull: false
}, },
expires_at: {
type: DataTypes.DATE,
allowNull: false
},
created_at: { created_at: {
type: DataTypes.DATE, type: DataTypes.DATE,
allowNull: false allowNull: false

View file

@ -9,9 +9,7 @@ async function login() {
if (!challengeResponse.ok) throw new Error("Failed to fetch challenge"); if (!challengeResponse.ok) throw new Error("Failed to fetch challenge");
const { challenge } = await challengeResponse.json(); const { challenge } = await challengeResponse.json();
console.log(`Received challenge: ${challenge}`); const pubkey = await window.nostr.getPublicKey();
} catch (error) { }
/* const pubkey = await window.nostr.getPublicKey();
const event = { const event = {
kind: 22242, kind: 22242,
@ -29,6 +27,9 @@ async function login() {
body: JSON.stringify(signedEvent), body: JSON.stringify(signedEvent),
}); });
} catch (error) { }
/*
if (!verifyResponse.ok) throw new Error("Verification failed"); if (!verifyResponse.ok) throw new Error("Verification failed");
const verifyResult = await verifyResponse.json(); const verifyResult = await verifyResponse.json();

View file

@ -1,5 +1,5 @@
const express = require('express'); const express = require('express');
//const { generatePrivateKey, getPublicKey, verifySignature } = require("nostr-tools"); const { getPublicKey, verifyEvent } = require("nostr-tools");
const crypto = require("crypto"); const crypto = require("crypto");
const appInviteService = require('../services/appInviteService'); const appInviteService = require('../services/appInviteService');
@ -64,4 +64,40 @@ router.get('/nostr-challenge', async (req, res) => {
res.json({ 'challenge': nostrChallenge.challenge }); res.json({ 'challenge': nostrChallenge.challenge });
}); });
router.post("/nostr-verify", async (req, res) => {
const signedEvent = req.body;
if (!signedEvent || !signedEvent.tags) {
return res.status(400).json({ success: false, error: "Invalid event format" });
}
const challengeTag = signedEvent.tags.find(tag => tag[0] === "challenge");
if (!challengeTag) {
return res.status(400).json({ success: false, error: "No challenge tag found" });
}
const challenge = challengeTag[1];
if (!(await nostrService.isNostrChallengeFresh(challenge))) {
return res.status(410).json({ success: false, error: "Challenge expired, request new one." })
}
if (await nostrService.hasNostrChallengeBeenCompleted(challenge)) {
return res.status(410).json({ success: false, error: "Challenge already used, request new one." })
}
const isSignatureValid = verifyEvent(signedEvent);
if (!isSignatureValid) {
return res.status(400).json({ success: false, error: "Invalid signature" });
}
await nostrService.completeNostrChallenge(
challenge,
signedEvent
)
return res.json({ success: true, signedEvent });
});
module.exports = router; module.exports = router;

View file

@ -1,15 +1,76 @@
const uuid = require("uuid"); const uuid = require("uuid");
const crypto = require("crypto"); const crypto = require("crypto");
const { Op } = require('sequelize');
const NostrChallengeCreated = require('../models/NostrChallengeCreated'); const NostrChallengeCreated = require('../models/NostrChallengeCreated');
const NostrChallengeCompleted = require("../models/NostrChallengeCompleted");
const constants = require('../constants');
async function createNostrChallenge() { async function createNostrChallenge() {
const currentTimestamp = new Date();
const expiryTimestamp = new Date(currentTimestamp.getTime());
expiryTimestamp.setSeconds(expiryTimestamp.getSeconds() + constants.DEFAULT_NOSTR_CHALLENGE_DURATION_SECONDS);
const nostrChallenge = await NostrChallengeCreated.create({ const nostrChallenge = await NostrChallengeCreated.create({
'uuid': uuid.v7(), 'uuid': uuid.v7(),
challenge: crypto.randomBytes(32).toString("hex"), challenge: crypto.randomBytes(32).toString("hex"),
created_at: new Date().toISOString() expires_at: expiryTimestamp.toISOString(),
created_at: currentTimestamp.toISOString()
}); });
return nostrChallenge; return nostrChallenge;
} }
exports.createNostrChallenge = createNostrChallenge; async function isNostrChallengeFresh(challengeString) {
const nostrChallenge = await NostrChallengeCreated.findOne({
where: {
challenge: challengeString,
expires_at: {
[Op.gt]: new Date()
}
}
});
if (nostrChallenge) {
return true;
}
return false;
}
async function hasNostrChallengeBeenCompleted(challengeString) {
const completedNostrChallenge = await NostrChallengeCompleted.findOne(
{
where: {
challenge: challengeString
}
}
);
if (completedNostrChallenge) {
return true;
}
return false;
}
async function completeNostrChallenge(
challenge,
signedEvent
) {
await NostrChallengeCompleted.create({
'uuid': uuid.v7(),
challenge: challenge,
signed_event: signedEvent,
created_at: new Date().toISOString()
}
);
return;
}
exports.createNostrChallenge = createNostrChallenge;
exports.isNostrChallengeFresh = isNostrChallengeFresh;
exports.hasNostrChallengeBeenCompleted = hasNostrChallengeBeenCompleted;
exports.completeNostrChallenge = completeNostrChallenge;