@bicycle-codes/webauthn-keys
v0.0.11
Published
Use ECC keys with the webauthn API
Downloads
36
Readme
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]
We only need 1 keypair 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.