0.0.11 • Published 9 months ago

@bicycle-codes/webauthn-keys v0.0.11

Weekly downloads
-
License
MIT
Repository
github
Last release
9 months ago

webauthn keys

tests types module semantic versioning install size license

A simple way to use crypto keys, protected by webauthn (biometric authentication).

Save an ECC keypair, then access it iff the user authenticates via webauthn.

See a live demo

install

npm i -S @bicycle-codes/webauthn-keys

how it works

We save the iv of the our keypair, which lets us re-create the same keypair on subsequent sessions.

The secret iv is set in the user.id property in a PublicKeyCredentialCreationOptions object. The browser saves the credential, and will only read it after successful authentication with the webauthn API.

!NOTE
We are not using the webcrypto API for creating keys, because we are waiting on ECC support in all browsers.

!NOTE(https://libsodium.gitbook.io/doc/quickstart#how-can-i-sign-and-encrypt-using-the-same-key-pair) for both signing and encrypting. Internally, we create 2 keypairs -- one for signing and one for encryption -- but this is hidden from the interface.

Use

This exposes ESM via package.json exports field.

ESM

import {
    create,
    getKeys,
    encrypt,
    decrypt,
    signData,
    verify,
    toBase64String,
    fromBase64String,
    localIdentities,
    storeLocalIdentities,
    pushLocalIdentity,
} from '@bicycle-codes/webauthn-keys'

// and types
import type {
    Identity,
    RegistrationResult,
    LockKey,
    JSONValue,
    AuthResponse
} from '@bicycle-codes/webauthn-keys'

pre-built JS

This package exposes minified JS files too. Copy them to a location that is accessible to your web server, then link to them in HTML.

copy

cp ./node_modules/@bicycle-codes/package/dist/index.min.js ./public/webauthn-keys.min.js

HTML

<script type="module" src="./webauthn-keys.min.js"></script>

example

Create a new keypair, and protect it with the webatuhn API.

import { create } from '@bicycle-codes/webauthn-keys'

const id = await create({
    username: 'alice',  // unique within relying party (this device)
    displayName: 'Alice Example',  // human-readable name
    relyingPartyName: 'Example application'  // rp.name
})

Save the public data of the new ID to indexedDB.

import { pushLocalIdentity } from '@bicycle-codes/webauthn-keys'

// save to indexedDB
await pushLocalIdentity(id.localID, id.record)

Login again, and get the same keypair in memory. This will prompt for biometric authentication.

import { auth, getKeys } from '@bicycle-codes/webauthn-keys'

const authResult = await auth()
const keys = getKeys(authResult)

See also

API

create

Create a new keypair. The relying party ID defaults to the current location.hostname.

async function create (
    lockKey = deriveLockKey(),
    opts:Partial<{
        username:string
        displayName:string
        relyingPartyID:string
        relyingPartyName:string
    }> = {
        username: 'local-user',
        displayName: 'Local User',
        relyingPartyID: document.location.hostname,
        relyingPartyName: 'wacg'
    }
):Promise<{ localID:string, record:Identity, keys:LockKey }>

create example

import {
    create,
    pushLocalIdentity
} from '@bicycle-codes/webauthn-keys'

const { record, keys, localID } = await create(undefined, {
    username: 'alice',
    displayName: 'Alice Example',
    relyingPartyID: location.hostname,
    relyingPartyName: 'Example application'
})

//
// Save the ID to indexedDB.
// This saves public info only, not keys.
//
await pushLocalIdentity(id.localID, record)

auth

Prompt the user for authentication with webauthn.

async function auth (
    opts:Partial<CredentialRequestOptions> = {}
):Promise<PublicKeyCredential & { response:AuthenticatorAssertionResponse }>

auth example

import { auth, getKeys } from '@bicycle-codes/webauthn'

const authResult = await auth()
const keys = getKeys(authResult)

pushLocalIdentity

Take the localId created by the create call, and save it to indexedDB.

async function pushLocalIdentity (localId:string, id:Identity):Promise<void>

pushLocalIdentity example

const id = await create({
    username,
    relyingPartyName: 'Example application'
})
await pushLocalIdentity(id.localID, id.record)

getKeys

Authenticate with a saved identity; takes the response from auth().

function getKeys (opts:(PublicKeyCredential & {
    response:AuthenticatorAssertionResponse
})):LockKey

getKeys example

import { getKeys, auth } from '@bicycle-codes/webauthn-keys'

// authenticate
const authData = await auth()

// get keys from auth response
const keys = getKeys(authData)

stringify

Return a base64 encoded string of the given public key.

function stringify (keys:LockKey):string

stringify example

import { stringify } from '@bicycle-codes/webauthn-keys'

const keyString = stringify(myKeys)
// => 'welOX9O96R6WH0S8cqqwMlPAJ3VwMgAZEnc1wa1MN70='

signData

export async function signData (data:string|Uint8Array, key:LockKey, opts?:{
    outputFormat?:'base64'|'raw'
}):Promise<Uint8Array>

signData example

import { signData, deriveLockKey } from '@bicycle-codes/webauthn-keys'

// create a new keypair
const key = await deriveLockKey()

const sig = await signData('hello world', key)
// => INZ2A9Lt/zL6Uf6d6D6fNi95xSGYDiUpK3tr/zz5a9iYyG5u...

verify

Check that the given signature is valid with the given data.

export async function verify (
    data:string|Uint8Array,
    sig:string|Uint8Array,
    keys:{ publicKey:Uint8Array|string }
):Promise<boolean>

verify example

import { verify } from '@bicycle-codes/webauthn-keys'

const isOk = await verify('hello', 'dxKmG3oTEN2i23N9d...', {
    publicKey: '...'  // Uint8Array or string
})
// => true

encrypt

export function encrypt (
    data:JSONValue,
    lockKey:LockKey,
    opts:{
        outputFormat:'base64'|'raw';
    } = { outputFormat: 'base64' }
// return type depends on the given output format
):string|Uint8Array

encrypt example

import { encrypt } from '@bicycle-codes/webauthn-keys'

const encrypted = encrypt('hello encryption', myKeys)
// => XcxWEwijaHq2u7aui6BBYGjIrjVTkLIS5...

decrypt

function decrypt (
    data:string|Uint8Array,
    lockKey:LockKey,
    opts:{ outputFormat?:'utf8'|'raw', parseJSON?:boolean } = {
        outputFormat: 'utf8',
        parseJSON: true
    }
):string|Uint8Array|JSONValue

decrypt example

import { decrypt } from '@bicycle-codes/webauthn-keys'

const decrypted = decrypt('XcxWEwijaHq2u7aui6B...', myKeys, {
    parseJSON: false
})

// => 'hello encryption'

localIdentities

Load local identities from indexed DB, return a dictionary from user ID to the identity record.

async function localIdentities ():Promise<Record<string, Identity>>

localIdentities example

import { localIdentites } from '@bicycle-codes/webauthn-keys'

const ids = await localIdentities()

develop

start a local server

npm start

test

Run some automated tests of the cryptography API, not webauthn.

start tests & watch for file changes

npm test

run tests and exit

npm run test:ci

see also

What's the WebAuthn User Handle (response.userHandle)?

Its primary function is to enable the authenticator to map a set of credentials (passkeys) to a specific user account.

A secondary use of the User Handle (response.userHandle) is to allow authenticators to know when to replace an existing resident key (discoverable credential) with a new one during the registration ceremony.

libsodium docs

credits

This is heavily influenced by @lo-fi/local-data-lock and @lo-fi/webauthn-local-client. Thanks @lo-fi organization and @getify for working in open source; this would not have been possible otherwise.

0.0.11

9 months ago

0.0.10

9 months ago

0.0.9

9 months ago

0.0.8

9 months ago

0.0.7

9 months ago

0.0.6

9 months ago

0.0.5

9 months ago

0.0.4

9 months ago

0.0.3

9 months ago

0.0.2

9 months ago

0.0.1

10 months ago

0.0.0

10 months ago