Skip to content

Commit

Permalink
feat: add endpoint to link public key to nano account
Browse files Browse the repository at this point in the history
  • Loading branch information
mistakia committed Apr 10, 2024
1 parent ad8425c commit 1f48df7
Show file tree
Hide file tree
Showing 4 changed files with 246 additions and 8 deletions.
2 changes: 1 addition & 1 deletion api/routes/auth/message.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ router.post('/?', async (req, res) => {
}

// public_key can be a linked keypair or an existing nano account
const linked_accounts = await db('accounts_keys')
const linked_accounts = await db('account_keys')
.select('account')
.where({ public_key })
const nano_account = tools.publicKeyToAddress(public_key)
Expand Down
61 changes: 61 additions & 0 deletions api/routes/auth/register.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ router.post('/?', async (req, res) => {
return res.status(401).send({ error: 'invalid username param' })
}

if (!nano.checkSignature(signature)) {
return res.status(401).send({ error: 'invalid signature' })
}

const valid_signature = ed25519.verify(signature, public_key, public_key)
if (!valid_signature) {
return res.status(401).send({ error: 'invalid signature' })
Expand Down Expand Up @@ -60,4 +64,61 @@ router.post('/?', async (req, res) => {
}
})

router.post('/key/?', async (req, res) => {
const { logger, db } = req.app.locals
try {
const required = ['public_key', 'signature', 'account']
for (const prop of required) {
if (!req.body[prop]) {
return res.status(400).send({ error: `missing ${prop} param` })
}
}

const { public_key, signature, account } = req.body

if (!nano.checkKey(public_key)) {
return res.status(401).send({ error: 'invalid public_key param' })
}

if (!nano.checkAddress(account)) {
return res.status(401).send({ error: 'invalid account param' })
}

if (!nano.checkSignature(signature)) {
return res.status(401).send({ error: 'invalid signature' })
}

const account_public_key = nano.derivePublicKey(account)
const valid_signature = ed25519.verify(
signature,
public_key,
account_public_key
)
if (!valid_signature) {
return res.status(401).send({ error: 'invalid signature' })
}

const created_at = Math.round(Date.now() / 1000)
await db('account_keys')
.insert({
account,
public_key,
signature,
created_at
})
.onConflict()
.ignore()

res.send({
account,
public_key,
signature,
created_at
})
} catch (error) {
logger(error)
res.status(500).send('Internal server error')
}
})

export default router
15 changes: 8 additions & 7 deletions db/schema.sql
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,17 @@ CREATE TABLE `accounts` (
-- --------------------------------------------------------

--
-- Table structure for table `accounts_keys`
-- Table structure for table `account_keys`
--

DROP TABLE IF EXISTS `accounts_keys`;
DROP TABLE IF EXISTS `account_keys`;

CREATE TABLE `accounts_keys` (
CREATE TABLE `account_keys` (
`account` char(65) CHARACTER SET utf8 NOT NULL,
`public_key` varchar(255) DEFAULT NULL,
`signature` varchar(255) DEFAULT NULL,
`created_at` int(11) DEFAULT NULL
`public_key` varchar(64) NOT NULL,
`signature` varchar(128) NOT NULL,
`created_at` int(11) NOT NULL,
UNIQUE `account_key` (`account`, `public_key`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4 COLLATE utf8mb4_unicode_ci;

-- --------------------------------------------------------
Expand Down Expand Up @@ -589,7 +590,7 @@ CREATE TABLE `users` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`username` varchar(32) NOT NULL,
`public_key` varchar(64) NOT NULL,
`signature` varchar(255) NOT NULL,
`signature` varchar(128) NOT NULL,
`last_visit` int(11) NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `public_key` (`public_key`)
Expand Down
176 changes: 176 additions & 0 deletions test/auth.register.key.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
/* global describe before it */
import chai from 'chai'
import chaiHTTP from 'chai-http'
import ed25519 from '@trashman/ed25519-blake2b'
import nano from 'nanocurrency'

import server from '#api/server.mjs'
import knex from '#db'
import { mochaGlobalSetup } from './global.mjs'

process.env.NODE_ENV = 'test'
// chai.should()
chai.use(chaiHTTP)
const expect = chai.expect

describe('API /auth/register/key', () => {
before(mochaGlobalSetup)

describe('POST /api/auth/register/key', () => {
it('should save the supplied public_key', async () => {
const private_key = Buffer.from(
'00000000000000000000000000000000000000000000000000000000000000000',
'hex'
)
const public_key = ed25519.publicKey(private_key)

const nano_account_private_key = Buffer.from(
'00000000000000000000000000000000000000000000000000000000000000001',
'hex'
)
const nano_account_public_key = ed25519.publicKey(
nano_account_private_key
)
const nano_account = nano.deriveAddress(
nano_account_public_key.toString('hex')
)

const signature = ed25519.sign(
public_key.toString('hex'),
nano_account_private_key,
nano_account_public_key
)

const response = await chai
.request(server)
.post('/api/auth/register/key')
.send({
public_key: public_key.toString('hex'),
signature: signature.toString('hex'),
account: nano_account
})

expect(response).to.have.status(200)

const saved_row = await knex('account_keys')
.where({ public_key: public_key.toString('hex') })
.first()

// eslint-disable-next-line no-unused-expressions
expect(saved_row).to.exist
expect(saved_row.account).to.equal(nano_account)
expect(saved_row.public_key).to.equal(public_key.toString('hex'))
expect(saved_row.signature).to.equal(signature.toString('hex'))
expect(saved_row.created_at).to.be.a('number')
expect(saved_row.created_at).to.equal(response.body.created_at)
})
})

describe('errors', () => {
it('should return 400 if public_key field is missing', async () => {
const response = await chai
.request(server)
.post('/api/auth/register/key')
.send({
signature: 'somesignature',
account: 'someaccount'
}) // missing public_key
expect(response).to.have.status(400)
expect(response.body.error).to.include('missing public_key param')
})

it('should return 400 if signature field is missing', async () => {
const response = await chai
.request(server)
.post('/api/auth/register/key')
.send({
public_key: 'somepub',
account: 'someaccount'
}) // missing signature
expect(response).to.have.status(400)
expect(response.body.error).to.include('missing signature param')
})

it('should return 400 if account field is missing', async () => {
const response = await chai
.request(server)
.post('/api/auth/register/key')
.send({
public_key: 'somepub',
signature: 'somesignature'
}) // missing account
expect(response).to.have.status(400)
expect(response.body.error).to.include('missing account param')
})

it('should return 401 if public_key param is invalid', async () => {
const response = await chai
.request(server)
.post('/api/auth/register/key')
.send({
public_key: 'invalidpub',
signature: 'somesignature',
account: 'someaccount'
})
expect(response).to.have.status(401)
expect(response.body.error).to.equal('invalid public_key param')
})

it('should return 401 if account param is invalid', async () => {
const private_key = Buffer.from(
'0000000000000000000000000000000000000000000000000000000000000000',
'hex'
)
const public_key = ed25519.publicKey(private_key)
const account = 'someaccount'
const signature = ed25519.sign(public_key, private_key, public_key)
const response = await chai
.request(server)
.post('/api/auth/register/key')
.send({
public_key: public_key.toString('hex'),
signature: signature.toString('hex'),
account
})
expect(response).to.have.status(401)
expect(response.body.error).to.equal('invalid account param')
})

it('should return 401 if signature is invalid', async () => {
const private_key = Buffer.from(
'0000000000000000000000000000000000000000000000000000000000000000',
'hex'
)
const public_key = ed25519.publicKey(private_key)

const nano_account = nano.deriveAddress(
'0000000000000000000000000000000000000000000000000000000000000001'
)

// private key used is different from the stated nano account
const nano_account_private_key = Buffer.from(
'0000000000000000000000000000000000000000000000000000000000000002',
'hex'
)
const nano_account_public_key = ed25519.publicKey(
nano_account_private_key
)
const signature = ed25519.sign(
public_key,
nano_account_private_key,
nano_account_public_key
)

const response = await chai
.request(server)
.post('/api/auth/register/key')
.send({
public_key: public_key.toString('hex'),
signature: signature.toString('hex'),
account: nano_account
})
expect(response).to.have.status(401)
expect(response.body.error).to.equal('invalid signature')
})
})
})

0 comments on commit 1f48df7

Please sign in to comment.