@bicycle-codes/webauthn-keys v0.0.11
webauthn keys
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
.
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
- Passkey vs. WebAuthn: What's the Difference?
- Discoverable credentials deep dive
- Sign in with a passkey through form autofill
- an opinionated, “quick-start” guide to using passkeys
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.