Skip to content

Commit

Permalink
Protect PIN validation from brute force attack (#96)
Browse files Browse the repository at this point in the history
* Protect PIN validation from brute force attack

Enables an attempt limit for the PIN number so that an attacker can't just try all possible PIN numbers

* Fix failing rollback
  • Loading branch information
mattdean-digicatapult authored Jul 11, 2024
1 parent 2c42920 commit 95e6511
Show file tree
Hide file tree
Showing 13 changed files with 216 additions and 24 deletions.
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "veritable-ui",
"version": "0.8.0",
"version": "0.8.1",
"description": "UI for Veritable",
"main": "src/index.ts",
"type": "module",
Expand Down
1 change: 1 addition & 0 deletions src/controllers/connection/__tests__/fixtures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ export const validConnection: ConnectionRow = {
status: 'pending',
company_number: validCompanyNumber,
company_name: 'must be a valid company name',
pin_attempt_count: 0,
}

const buildBase64Invite = (companyNumber: string) =>
Expand Down
4 changes: 4 additions & 0 deletions src/controllers/connection/__tests__/newConnection.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,7 @@ describe('NewConnectionController', () => {
company_number: '00000001',
status: 'pending',
agent_connection_id: null,
pin_attempt_count: 0,
},
])
})
Expand All @@ -329,6 +330,7 @@ describe('NewConnectionController', () => {
connection_id: '42',
oob_invite_id: 'id-NAME',
expires_at: new Date(100 + 14 * 24 * 60 * 60 * 1000),
validity: 'valid',
})
expect(typeof pin_hash).to.equal('string')
})
Expand Down Expand Up @@ -508,6 +510,7 @@ describe('NewConnectionController', () => {
company_number: '00000001',
status: 'pending',
agent_connection_id: 'oob-connection',
pin_attempt_count: 0,
},
])
})
Expand All @@ -519,6 +522,7 @@ describe('NewConnectionController', () => {
connection_id: '42',
oob_invite_id: 'oob-record',
expires_at: new Date(100 + 14 * 24 * 60 * 60 * 1000),
validity: 'valid',
})
expect(typeof pin_hash).to.equal('string')
})
Expand Down
2 changes: 2 additions & 0 deletions src/controllers/connection/newConnection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -331,13 +331,15 @@ export class NewConnectionController extends HTMLController {
company_number: company.company_number,
agent_connection_id: agentConnectionId,
status: 'pending',
pin_attempt_count: 0,
})

await db.insert('connection_invite', {
connection_id: record.id,
oob_invite_id: invitationId,
pin_hash: pinHash,
expires_at: new Date(new Date().getTime() + 14 * 24 * 60 * 60 * 1000),
validity: 'valid',
})
connectionId = record.id
})
Expand Down
1 change: 1 addition & 0 deletions src/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ export const envConfig = {
CLOUDAGENT_ADMIN_ORIGIN: envalid.url({ devDefault: 'http://localhost:3100' }),
CLOUDAGENT_ADMIN_WS_ORIGIN: envalid.url({ devDefault: 'ws://localhost:3100' }),
INVITATION_PIN_SECRET: pinSecretValidator({ devDefault: Buffer.from('secret', 'utf8') }),
INVITATION_PIN_ATTEMPT_LIMIT: envalid.num({ default: 5 }),
INVITATION_FROM_COMPANY_NUMBER: envalid.str({ devDefault: '07964699' }),
ISSUANCE_DID_POLICY: issuanceRecordValidator({ devDefault: 'EXISTING_OR_NEW' }),
ISSUANCE_SCHEMA_POLICY: issuanceRecordValidator({ devDefault: 'EXISTING_OR_NEW' }),
Expand Down
14 changes: 13 additions & 1 deletion src/models/db/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { container, singleton } from 'tsyringe'
import { z } from 'zod'

import { Env } from '../../env.js'
import Zod, { IDatabase, Models, Order, TABLE, Update, Where, tablesList } from './types.js'
import Zod, { ColumnsByType, IDatabase, Models, Order, TABLE, Update, Where, tablesList } from './types.js'
import { reduceWhere } from './util.js'

const env = container.resolve(Env)
Expand Down Expand Up @@ -67,6 +67,18 @@ export default class Database {
return z.array(Zod[model].get).parse(await query.returning('*'))
}

increment = async <M extends TABLE>(
model: M,
column: ColumnsByType<M, number>,
where?: Where<M>,
amount: number = 1
): Promise<Models[typeof model]['get'][]> => {
let query = this.db[model]()
query = reduceWhere(query, where)
query = query.increment(column, amount)
return z.array(Zod[model].get).parse(await query.returning('*'))
}

get = async <M extends TABLE>(
model: M,
where?: Where<M>,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,6 @@ export async function up(knex: Knex): Promise<void> {
}

export async function down(knex: Knex): Promise<void> {
await knex.raw('DROP INDEX company_name_trgm_idx on connection')
await knex.raw('DROP INDEX company_name_trgm_idx')
await knex.raw('DROP EXTENSION "pg_trgm"')
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import type { Knex } from 'knex'

export async function up(knex: Knex): Promise<void> {
await knex.schema.alterTable('connection', (def) => {
def.tinyint('pin_attempt_count').unsigned().notNullable().defaultTo(0)
})

await knex.schema.alterTable('connection_invite', (def) => {
def
.enum('validity', ['valid', 'expired', 'too_many_attempts', 'used'], {
useNative: true,
enumName: 'connection_invite_verification_status',
})
.defaultTo('valid')
})
}

export async function down(knex: Knex): Promise<void> {
await knex.schema.alterTable('connection', (def) => {
def.dropColumn('pin_attempt_count')
})

await knex.schema.alterTable('connection_invite', (def) => {
def.dropColumn('validity')
})

await knex.raw('DROP TYPE connection_invite_verification_status')
}
7 changes: 7 additions & 0 deletions src/models/db/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,15 @@ const insertConnection = z.object({
z.literal('disconnected'),
]),
agent_connection_id: z.union([z.string(), z.null()]),
pin_attempt_count: z.number().int().gte(0).lte(255),
})

const insertConnectionInvite = z.object({
connection_id: z.string(),
oob_invite_id: z.string(),
pin_hash: z.string(),
expires_at: z.date(),
validity: z.union([z.literal('valid'), z.literal('expired'), z.literal('too_many_attempts'), z.literal('used')]),
})
const insertQuery = z.object({
connection_id: z.string(),
Expand Down Expand Up @@ -74,6 +76,10 @@ export type Models = {
}
}

export type ColumnsByType<M extends TABLE, T> = {
[K in keyof Models[M]['get']]-?: Models[M]['get'][K] extends T ? K : never
}[keyof Models[M]['get']]

type WhereComparison<M extends TABLE> = {
[key in keyof Models[M]['get']]: [
Extract<key, string>,
Expand All @@ -88,6 +94,7 @@ export type WhereMatch<M extends TABLE> = {
export type Where<M extends TABLE> = WhereMatch<M> | (WhereMatch<M> | WhereComparison<M>[keyof Models[M]['get']])[]
export type Order<M extends TABLE> = [keyof Models[M]['get'], 'asc' | 'desc'][]
export type Update<M extends TABLE> = Partial<Models[M]['get']>

export type IDatabase = {
[key in TABLE]: () => Knex.QueryBuilder
}
Expand Down
Loading

0 comments on commit 95e6511

Please sign in to comment.