@bicycle-codes/webauthn-keys v0.1.1
webauthn keys
A simple way to use crypto keys with webauthn (biometric authentication).
Save an ECC keypair, then access it iff the user authenticates via webauthn.
install
npm i -S @bicycle-codes/webauthn-keyshow 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.
get started
first session
Create a new keypair.
import { create } from '@bicycle-codes/webauthn-keys'
const id = await create({ // create a new user
username,
relyingPartyName: 'Example application'
})Save the new user to indexedDB
import { pushLocalIdentity } from '@bicycle-codes/webauthn-keys'
// save the user to `indexedDB`
await pushLocalIdentity(id.localID, id.record)Login with this user
import { auth } from '@bicycle-codes/webauthn-keys'
// ... sometime in the future, login again ...
const localID = buttonElement.dataset.localId
const authResult = await auth(localID!)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.jsHTML
Link to the file you copied.
<script type="module" src="./webauthn-keys.min.js"></script>example
Create a new keypair
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 public data to indexedDB
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)get a persisted keypair
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
develop
!TIP You can use the browser dev tools to setup a virtual authenticator
start a local server
npm startAPI
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
})):LockKeygetKeys 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):stringstringify 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
})
// => trueencrypt
export function encrypt (
data:JSONValue,
lockKey:LockKey,
opts:{
outputFormat:'base64'|'raw';
} = { outputFormat: 'base64' }
// return type depends on the given output format
):string|Uint8Arrayencrypt 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|JSONValuedecrypt 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()test
Run some automated tests of the cryptography API, not webauthn.
start tests & watch for file changes
npm testrun tests and exit
npm run test:cisee 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.