Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

adds changes per webauthn audit #42

Merged
merged 4 commits into from
Jun 22, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 20 additions & 6 deletions src/identity/webauthn/webauthn-identity.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import cbor from "cbor"
import { ONE_MINUTE } from "../../const"
import { CoseKey, EMPTY } from "../../message/cose"
import { makeRandomBytes } from "../../utils"
import { Address } from "../address"
import { PublicKeyIdentity } from "../types"
const sha512 = require("js-sha512")

const CHALLENGE_BUFFER = new TextEncoder().encode("lifted")

export class WebAuthnIdentity extends PublicKeyIdentity {
publicKey: ArrayBuffer
rawId: ArrayBuffer
Expand All @@ -29,6 +28,7 @@ export class WebAuthnIdentity extends PublicKeyIdentity {
}

static async create(): Promise<WebAuthnIdentity> {
checkBrowserSupport()
const publicKeyCredential = await createPublicKeyCredential()
const attestationResponse = publicKeyCredential?.response
if (!(attestationResponse instanceof AuthenticatorAttestationResponse)) {
Expand Down Expand Up @@ -56,11 +56,12 @@ export class WebAuthnIdentity extends PublicKeyIdentity {
credentialId: ArrayBuffer,
challenge?: Uint8Array | ArrayBuffer,
): Promise<PublicKeyCredential> {
checkBrowserSupport()
let credential = (await window.navigator.credentials.get({
publicKey: {
challenge: challenge ?? CHALLENGE_BUFFER,
challenge: challenge ?? makeRandomBytes(),
timeout: ONE_MINUTE,
userVerification: "discouraged",
userVerification: "preferred",
allowCredentials: [
{
transports: ["nfc", "usb", "ble"],
Expand Down Expand Up @@ -109,7 +110,14 @@ export class WebAuthnIdentity extends PublicKeyIdentity {
}
}

export async function createPublicKeyCredential(challenge = CHALLENGE_BUFFER) {
async function createPublicKeyCredential(challenge = makeRandomBytes()) {
if (
!(challenge?.buffer instanceof ArrayBuffer) ||
!challenge?.BYTES_PER_ELEMENT ||
challenge?.byteLength < 32
) {
throw new Error("invalid challenge")
}
const publicKey: PublicKeyCredentialCreationOptions = {
challenge,

Expand All @@ -118,7 +126,7 @@ export async function createPublicKeyCredential(challenge = CHALLENGE_BUFFER) {
},

user: {
id: window.crypto.getRandomValues(new Uint8Array(32)),
id: makeRandomBytes(),
name: "Lifted",
displayName: "Lifted",
},
Expand Down Expand Up @@ -158,3 +166,9 @@ function getCosePublicKey(authData: ArrayBuffer): ArrayBuffer {
const cosePublicKey = authData.slice(55 + credentialIdLength)
return cosePublicKey
}

function checkBrowserSupport() {
if (!window.PublicKeyCredential) {
throw new Error("Webauthn not supported")
}
}
2 changes: 1 addition & 1 deletion src/message/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ export class Message {
content.set(6, obj.id);
}
if (obj.nonce) {
content.set(7, obj.nonce);
content.set(7, cbor.encode(obj.nonce))
}
if (obj.attrs) {
content.set(8, obj.attrs);
Expand Down
6 changes: 5 additions & 1 deletion src/network/modules/account/__tests__/account.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,9 @@ describe("Account", () => {
})

it("should submit multisig transactions", async () => {
const opts = {
nonce: new ArrayBuffer(16),
}
const resMultisigToken = new Uint8Array()
const mockCall = jest.fn(async () => {
return makeMockResponseMessage(new Map().set(0, resMultisigToken))
Expand All @@ -92,7 +95,7 @@ describe("Account", () => {
memo: "this is a memo",
}

const res = await account.submitMultisigTxn(EventType.send, txnData)
const res = await account.submitMultisigTxn(EventType.send, txnData, opts)

const expectedCallArgs = new Map()
.set(0, txnData.from)
Expand All @@ -106,6 +109,7 @@ describe("Account", () => {
expect(mockCall).toHaveBeenCalledWith(
"account.multisigSubmitTransaction",
expectedCallArgs,
opts,
)
expect(res).toEqual({ token: resMultisigToken })
})
Expand Down
7 changes: 6 additions & 1 deletion src/network/modules/account/account.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
getAddressFromTaggedIdentity,
makeAccountInfoData,
makeLedgerSendParam,
makeRandomBytes,
makeTxnData,
} from "../../../utils"
import { Event } from "../events"
Expand All @@ -26,6 +27,7 @@ export interface Account extends NetworkModule {
submitMultisigTxn: (
txnType: EventType,
txnData: SubmitMultisigTxnData,
opts: { nonce?: ArrayBuffer },
) => Promise<GetMultisigTokenReturnType>
multisigInfo: (token: ArrayBuffer) => Promise<unknown>
multisigApprove: (token: ArrayBuffer) => Promise<unknown>
Expand Down Expand Up @@ -66,12 +68,15 @@ export const Account: Account = {
async submitMultisigTxn(
txnType: EventType,
txnData: SubmitMultisigTxnData,
{ nonce = makeRandomBytes(16) },
): Promise<GetMultisigTokenReturnType> {
const m = new Map()
m.set(0, txnData.from)
txnData?.memo && m.set(1, txnData.memo)
m.set(2, makeSubmittedTxnData(txnType, txnData))
const msg = await this.call("account.multisigSubmitTransaction", m)
const msg = await this.call("account.multisigSubmitTransaction", m, {
nonce,
})
return getMultisigToken(msg)
},

Expand Down
23 changes: 18 additions & 5 deletions src/network/modules/id-store/__tests__/id-store.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,23 +19,33 @@ describe("IdStore", () => {
"ma123",
new ArrayBuffer(32),
new ArrayBuffer(32),
{
nonce: new ArrayBuffer(16),
},
)
expect(mockCall).toHaveBeenCalledTimes(1)
expect(mockCall).toHaveBeenCalledWith("idstore.store", m)
expect(mockCall).toHaveBeenCalledWith("idstore.store", m, {
nonce: new ArrayBuffer(16),
})
expect(res).toEqual({ phrase: "recovery phrase" })
})
})
describe("idstore.getFromRecallPhrase()", () => {
it("returns cosePublicKey and credentialId", async () => {
const opts = { nonce: new ArrayBuffer(16) }
const mockCall = jest.fn(async () => {
return mockGetCredentialResponseMessage
})
const m = new Map()
m.set(0, ["recall", "phrase"])
const idStore = setupIdStore(mockCall)
const res = await idStore.getFromRecallPhrase("recall phrase")
const res = await idStore.getFromRecallPhrase("recall phrase", opts)
expect(mockCall).toHaveBeenCalledTimes(1)
expect(mockCall).toHaveBeenCalledWith("idstore.getFromRecallPhrase", m)
expect(mockCall).toHaveBeenCalledWith(
"idstore.getFromRecallPhrase",
m,
opts,
)
expect(res).toEqual({
credentialId: Buffer.from(new ArrayBuffer(32)),
cosePublicKey: Buffer.from(new ArrayBuffer(32)),
Expand All @@ -44,15 +54,18 @@ describe("IdStore", () => {
})
describe("idstore.getFromAddress()", () => {
it("returns cosePublicKey and credentialId", async () => {
const opts = {
nonce: new ArrayBuffer(16),
}
const mockCall = jest.fn(async () => {
return mockGetCredentialResponseMessage
})
const m = new Map()
m.set(0, "ma123")
const idStore = setupIdStore(mockCall)
const res = await idStore.getFromAddress("ma123")
const res = await idStore.getFromAddress("ma123", opts)
expect(mockCall).toHaveBeenCalledTimes(1)
expect(mockCall).toHaveBeenCalledWith("idstore.getFromAddress", m)
expect(mockCall).toHaveBeenCalledWith("idstore.getFromAddress", m, opts)
expect(res).toEqual({
credentialId: Buffer.from(new ArrayBuffer(32)),
cosePublicKey: Buffer.from(new ArrayBuffer(32)),
Expand Down
31 changes: 26 additions & 5 deletions src/network/modules/id-store/id-store.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import cbor from "cbor"
import { Message } from "../../../message"
import { makeRandomBytes } from "../../../utils"
import type { NetworkModule } from "../types"

export type GetPhraseReturnType = ReturnType<typeof getPhrase>
Expand All @@ -11,9 +11,16 @@ interface IdStore extends NetworkModule {
address: string,
credId: ArrayBuffer,
credCosePublicKey: ArrayBuffer,
opts: { nonce?: ArrayBuffer },
) => Promise<GetPhraseReturnType>
getFromRecallPhrase: (phrase: string) => Promise<GetCredentialDataReturnType>
getFromAddress: (address: string) => Promise<GetCredentialDataReturnType>
getFromRecallPhrase: (
phrase: string,
opts: { nonce?: ArrayBuffer },
) => Promise<GetCredentialDataReturnType>
getFromAddress: (
address: string,
opts: { nonce?: ArrayBuffer },
) => Promise<GetCredentialDataReturnType>
}

export const IdStore: IdStore = {
Expand All @@ -23,30 +30,44 @@ export const IdStore: IdStore = {
address: string,
credId: ArrayBuffer,
credCosePublicKey: ArrayBuffer,
opts = {},
): Promise<GetPhraseReturnType> {
const { nonce } = opts
const m = new Map()
m.set(0, address)
m.set(1, Buffer.from(credId))
m.set(2, credCosePublicKey)
const message = await this.call("idstore.store", m)
const message = await this.call("idstore.store", m, {
nonce,
})
return getPhrase(message)
},

async getFromRecallPhrase(
phrase: string,
{ nonce = makeRandomBytes(16) },
): Promise<GetCredentialDataReturnType> {
const val = phrase.trim().split(" ")
const message = await this.call(
"idstore.getFromRecallPhrase",
new Map([[0, val]]),
{
nonce,
},
)
return getCredentialData(message)
},

async getFromAddress(address: string): Promise<GetCredentialDataReturnType> {
async getFromAddress(
address: string,
{ nonce = makeRandomBytes(16) },
): Promise<GetCredentialDataReturnType> {
const message = await this.call(
"idstore.getFromAddress",
new Map([[0, address]]),
{
nonce,
},
)
return getCredentialData(message)
},
Expand Down
14 changes: 10 additions & 4 deletions src/network/modules/ledger/ledger.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Address } from "../../../identity"
import { Message } from "../../../message"
import { makeLedgerSendParam } from "../../../utils"
import { makeLedgerSendParam, makeRandomBytes } from "../../../utils"
import { LedgerSendParam, NetworkModule } from "../types"

export interface LedgerInfo {
Expand All @@ -13,7 +13,10 @@ interface Ledger extends NetworkModule {
balance: (address?: string, symbols?: string[]) => Promise<Balances>
mint: () => Promise<unknown>
burn: () => Promise<unknown>
send: (data: LedgerSendParam) => Promise<unknown>
send: (
data: LedgerSendParam,
opts: { nonce?: ArrayBuffer },
) => Promise<unknown>
}

export const Ledger: Ledger = {
Expand All @@ -37,8 +40,11 @@ export const Ledger: Ledger = {
throw new Error("Not implemented")
},

async send(param: LedgerSendParam): Promise<unknown> {
return await this.call("ledger.send", makeLedgerSendParam(param))
async send(
param: LedgerSendParam,
{ nonce = makeRandomBytes(16) },
): Promise<unknown> {
return await this.call("ledger.send", makeLedgerSendParam(param), { nonce })
},
}

Expand Down
5 changes: 3 additions & 2 deletions src/network/network.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Identity } from "../identity"
import { Message } from "../message"
import { CborData } from "../message/cbor"
import { applyMixins, throwOnErrorResponse } from "../utils"
import { applyMixins } from "../utils"
import { NetworkModule } from "./modules"
import { Async } from "./modules/async"

Expand Down Expand Up @@ -40,11 +40,12 @@ export class Network {
return Buffer.from(reply)
}

async call(method: string, data?: any) {
async call(method: string, data?: any, opts = {}) {
const req = Message.fromObject({
method,
from: this.identity ? await this.identity.getAddress() : undefined,
data,
...opts,
})
return await this.send(req)
}
Expand Down
5 changes: 5 additions & 0 deletions src/utils/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// import crypto from "crypto"
import { Message } from "../message"
import { ManyError, SerializedManyError } from "../message/error"
import { Network, NetworkModule } from "../network"
Expand Down Expand Up @@ -35,3 +36,7 @@ export function throwOnErrorResponse(msg: Message) {
}
return msg
}

export function makeRandomBytes(size = 32) {
return crypto.getRandomValues(new Uint8Array(size))
}