Use ECC keys with the webauthn API
npm install @substrate-system/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.
Contents
- install
- how it works
- get started
* first session
- Use
* ESM
* pre-built JS
- example
* Create a new keypair
* Save public data to indexedDB
* get a persisted keypair
* See also
- develop
* start a local server
- API
* create
* auth
* pushLocalIdentity
* getKeys
* stringify
* signData
* verify
* encrypt
* decrypt
* localIdentities
- test
* start tests & watch for file changes
* run tests and exit
- see also
* What's the WebAuthn User Handle (response.userHandle)?
* libsodium docs
- credits
``sh`
npm i -S @substrate-system/webauthn-keys
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 awebauthn
PublicKeyCredentialCreationOptions
object. The browser saves the credential, and will only read it after
successful authentication with the 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.
Create a new keypair.
`js
import { create } from '@substrate-system/webauthn-keys'
const id = await create({ // create a new user
username: 'alice'
})
`
Save the new user to indexedDB
`js
import { pushLocalIdentity } from '@substrate-system/webauthn-keys'
await pushLocalIdentity(id.localID, id.record)
`
Login with this user
`js
import { auth } from '@substrate-system/webauthn-keys'
// ... sometime in the future, login again ...
const localID = buttonElement.dataset.localId
const authResult = await auth(localID!)
`
------------------------------------------------------------------
field.js
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'
`$3
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
`sh
cp ./node_modules/@substrate-system/webauthn-keys/dist/index.min.js ./public/webauthn-keys.min.js
`#### HTML
Link to the file you copied.
`html
`------------------------------------------------------------------
example
$3
Create a new keypair, and keep it secret with the
webatuhn API.`ts
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
})
`$3
Save the public data of the new ID to
indexedDB:`ts
import { pushLocalIdentity } from '@substrate-system/webauthn-keys'// save to indexedDB
await pushLocalIdentity(id.localID, id.record)
`$3
Login again, and get the same keypair in memory. This will prompt for biometric authentication.
`ts
import { auth, getKeys } from '@substrate-system/webauthn-keys'const authResult = await auth()
const keys = getKeys(authResult)
`$3
* username property
* displayName property
* What's the Difference Between User Name and User Display Name?
-------------------------------------------------------------------------
develop
>
> [!TIP]
> You can use the browser dev tools to setup a virtual authenticator
>
$3
`sh
npm start
`-------------------------------------------------------------------
API
$3
Create a new keypair. The relying party ID defaults to the current location.hostname.`ts
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`js
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)
`$3
Prompt the user for authentication with webauthn.`ts
async function auth (
opts:Partial = {}
):Promise
`####
auth example`ts
import { auth, getKeys } from '@substrate-system/webauthn'const authResult = await auth()
const keys = getKeys(authResult)
`$3
Take the localId created by the create call, and save it to indexedDB.`ts
async function pushLocalIdentity (localId:string, id:Identity):Promise
`####
pushLocalIdentity example
`ts
const id = await create({
username,
relyingPartyName: 'Example application'
})
await pushLocalIdentity(id.localID, id.record)
`
$3
Authenticate with a saved identity; takes the response from auth().`ts
function getKeys (opts:(PublicKeyCredential & {
response:AuthenticatorAssertionResponse
})):LockKey
`####
getKeys example`ts
import { getKeys, auth } from '@substrate-system/webauthn-keys'// authenticate
const authData = await auth()
// get keys from auth response
const keys = getKeys(authData)
`$3
Return a base64 encoded string of the given public key.`ts
function stringify (keys:LockKey):string
`####
stringify example
`ts
import { stringify } from '@substrate-system/webauthn-keys'const keyString = stringify(myKeys)
// => 'welOX9O96R6WH0S8cqqwMlPAJ3VwMgAZEnc1wa1MN70='
`$3
`ts
export async function signData (data:string|Uint8Array, key:LockKey, opts?:{
outputFormat?:'base64'|'raw'
}):Promise
`####
signData example
`ts
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...
`$3
Check that the given signature is valid with the given data.`ts
export async function verify (
data:string|Uint8Array,
sig:string|Uint8Array,
keys:{ publicKey:Uint8Array|string }
):Promise
`####
verify example
`ts
import { verify } from '@substrate-system/webauthn-keys'const isOk = await verify('hello', 'dxKmG3oTEN2i23N9d...', {
publicKey: '...' // Uint8Array or string
})
// => true
`$3
`ts
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
`js
import { encrypt } from '@substrate-system/webauthn-keys'const encrypted = encrypt('hello encryption', myKeys)
// => XcxWEwijaHq2u7aui6BBYGjIrjVTkLIS5...
`$3
`ts
function decrypt (
data:string|Uint8Array,
lockKey:LockKey,
opts:{ outputFormat?:'utf8'|'raw', parseJSON?:boolean } = {
outputFormat: 'utf8',
parseJSON: true
}
):string|Uint8Array|JSONValue
`####
decrypt example`js
import { decrypt } from '@substrate-system/webauthn-keys'const decrypted = decrypt('XcxWEwijaHq2u7aui6B...', myKeys, {
parseJSON: false
})
// => 'hello encryption'
`$3
Load local identities from indexed DB, return a dictionary from user ID to the identity record.`ts
async function localIdentities ():Promise>
`####
localIdentities example`js
import { localIdentites } from '@substrate-system/webauthn-keys'const ids = await localIdentities()
`
-----------------------------------------------------------------------
test
Run some automated tests of the cryptography API, not webauthn.$3
`sh
npm test
`$3
`sh
npm run test:ci
``
--------------------------------------------------------------------------
* 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
> 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.
* How can I sign and encrypt using the same key pair?
------------------------------------------------------------------------
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.