@bicycle-codes/webauthn-keys
v0.1.3
Published
Use ECC keys with the webauthn API
Readme
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 @substrate-system/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] 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.
get started
first session
Create a new keypair.
import { create } from '@substrate-system/webauthn-keys'
const id = await create({ // create a new user
username: 'alice'
})Save the new user to indexedDB
import { pushLocalIdentity } from '@substrate-system/webauthn-keys'
await pushLocalIdentity(id.localID, id.record)Login with this user
import { auth } from '@substrate-system/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 '@substrate-system/webauthn-keys'
// and types
import type {
Identity,
RegistrationResult,
LockKey,
JSONValue,
AuthResponse
} from '@substrate-system/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/@substrate-system/webauthn-keys/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 keep it secret with the webatuhn API.
import { create } from '@substrate-system/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. Default is domain name
})Save public data to indexedDB
Save the public data of the new ID to indexedDB:
import { pushLocalIdentity } from '@substrate-system/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 '@substrate-system/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 '@substrate-system/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 '@substrate-system/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 '@substrate-system/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 '@substrate-system/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 '@substrate-system/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 '@substrate-system/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 '@substrate-system/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 '@substrate-system/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 '@substrate-system/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.
