@lo-fi/local-data-lock
Advanced tools
Comparing version 0.9.5 to 0.10.0
/*! Local-Data-Lock: ldl.js | ||
v0.9.5 (c) 2024 Kyle Simpson | ||
v0.10.0 (c) 2024 Kyle Simpson | ||
MIT License: http://getify.mit-license.org | ||
*/ | ||
import{supportsWebAuthn as e,regDefaults as t,register as r,authDefaults as n,auth as a,verifyAuthResponse as i,packPublicKeyJSON as o,unpackPublicKeyJSON as s,toBase64String as c,fromBase64String as l,toUTF8String as y,fromUTF8String as u,resetAbortReason as p}from"@lo-fi/webauthn-local-client";const d=sodium.crypto_sign_SEEDBYTES;var f=setMaxLockKeyCacheLifetime(),k=function loadLocalIdentities(){return Object.fromEntries(Object.entries(JSON.parse(window.localStorage.getItem("local-identities")||null)||{}).filter((([e,t])=>"number"==typeof t.lastSeq&&Array.isArray(t.passkeys)&&t.passkeys.length>0&&t.passkeys.every((e=>"string"==typeof e.credentialID&&""!=e.credentialID&&"number"==typeof e.seq&&null!=e.publicKey&&"object"==typeof e.publicKey&&"number"==typeof e.publicKey.algoCOSE&&"string"==typeof e.publicKey.raw&&""!=e.publicKey.raw&&"string"==typeof e.publicKey.spki&&""!=e.publicKey.spki&&"string"==typeof e.hash&&""!=e.hash&&e.hash==computePasskeyEntryHash(e))))).map((([e,t])=>[e,{...t,passkeys:t.passkeys.map((e=>({...e,publicKey:s(e.publicKey)})))}])))}(),b={},m=null;export{e as supportsWebAuthn,o as packPublicKeyJSON,s as unpackPublicKeyJSON,c as toBase64String,l as fromBase64String,y as toUTF8String,u as fromUTF8String,listLocalIdentities,clearLockKeyCache,removeLocalAccount,getLockKey,generateEntropy,deriveLockKey,lockData,unlockData,setMaxLockKeyCacheLifetime};var K={supportsWebAuthn:e,packPublicKeyJSON:o,unpackPublicKeyJSON:s,toBase64String:c,fromBase64String:l,toUTF8String:y,fromUTF8String:u,listLocalIdentities:listLocalIdentities,clearLockKeyCache:clearLockKeyCache,removeLocalAccount:removeLocalAccount,getLockKey:getLockKey,generateEntropy:generateEntropy,deriveLockKey:deriveLockKey,lockData:lockData,unlockData:unlockData,setMaxLockKeyCacheLifetime:setMaxLockKeyCacheLifetime};export default K;function listLocalIdentities(){return Object.keys(k)}function cacheLockKey(e,t,r=!1){e in b&&!r||(b[e]={...t,timestamp:Date.now()})}function clearLockKeyCache(e){null!=e?delete b[e]:b={}}function removeLocalAccount(e){delete b[e],delete k[e],storeLocalIdentities()}async function getLockKey({localIdentity:e=c(generateEntropy(15)),username:o="local-user",displayName:s="Local User",relyingPartyID:l=document.location.hostname,relyingPartyName:y="Local Data Lock",addNewPasskey:u=!1,resetLockKey:p=!1,verify:K=!0}={}){var g=null!=e?k[e]:null;if(null!=g){let t=function getCachedLockKey(e){if(e in b&&b[e].timestamp>=Date.now()-f){let{timestamp:t,...r}=b[e];return r}}(e);if(null==t||p){if(delete b[e],p)return resetAbortToken(),({record:k[e],lockKey:t}=await registerLocalIdentity()),storeLocalIdentities(),cacheLockKey(e,t),{...t,localIdentity:e};if(u)throw new Error("Encryption/Decryption key not currently cached, unavailable for new passkey");{resetAbortToken();let t=n({relyingPartyID:l,mediation:"optional",allowCredentials:g.passkeys.map((({credentialID:e})=>({type:"public-key",id:e}))),signal:m.signal}),r=await a(t);if(K){let e=g.passkeys.find((e=>e.credentialID==r.response.credentialID)),t=null!=e?e.publicKey:null;if(!(null!=t&&await i(r.response,t)))throw new Error("Auth verification failed")}return{...extractLockKey(r),localIdentity:e}}}if(u){resetAbortToken();let{record:e}=await registerLocalIdentity(t);g.lastSeq=e.lastSeq,g.passkeys=[...g.passkeys,...e.passkeys],storeLocalIdentities()}return{...t,localIdentity:e}}if(u){resetAbortToken();let{record:t,lockKey:r}=await registerLocalIdentity();return k[e]=t,cacheLockKey(e,r),storeLocalIdentities(),{...r,localIdentity:e}}{resetAbortToken();let t=n({relyingPartyID:l,mediation:"optional",signal:m.signal}),r=await a(t),o=extractLockKey(r),[s]=Object.entries(k).find((([,e])=>null!=e.passkeys.find((e=>e.credentialID==r.response.credentialID))))||[];if(null!=s){if(delete b[e],g=k[e=s],K){let e=g.passkeys.find((e=>e.credentialID==r.response.credentialID)),t=null!=e?e.publicKey:null;if(!(null!=t&&await i(r.response,t)))throw new Error("Auth verification failed")}cacheLockKey(e,o)}else if(K)throw new Error("Auth verification requested but skipped, against unrecognized passkey (no matching local-identity)");return{...o,localIdentity:e}}async function registerLocalIdentity(n=deriveLockKey()){try{let i=((k[e]||{}).lastSeq||0)+1,c=new Uint8Array(n.iv.byteLength+2),u=new DataView(new ArrayBuffer(2));u.setInt16(0,i,!1),c.set(n.iv,0),c.set(new Uint8Array(u.buffer),n.iv.byteLength);let p=t({relyingPartyID:l,relyingPartyName:y,user:{id:c,name:o,displayName:s},signal:m.signal}),d=await r(p);return{record:{lastSeq:i,passkeys:[(a={seq:i,credentialID:d.response.credentialID,publicKey:d.response.publicKey},{...a,hash:computePasskeyEntryHash(a)})]},lockKey:n}}catch(e){throw console.log(e),new Error("Identity/Passkey registration failed",{cause:e})}var a}function extractLockKey(t){try{if(t&&t.response&&isByteArray(t.response.userID)&&t.response.userID.byteLength==d+2){let r=deriveLockKey(t.response.userID.subarray(0,d));return cacheLockKey(e,r),r}throw new Error("Passkey info missing")}catch(e){throw new Error("Chosen passkey did not provide a valid encryption/decryption key",{cause:e})}}}function resetAbortToken(){m&&m.abort("Passkey operation abandoned."),m=new AbortController}function generateEntropy(e=16){return sodium.randombytes_buf(e)}function deriveLockKey(e=generateEntropy(d)){try{let t=sodium.crypto_sign_seed_keypair(e);return{iv:e,publicKey:t.publicKey,privateKey:t.privateKey,encPK:sodium.crypto_sign_ed25519_pk_to_curve25519(t.publicKey),encSK:sodium.crypto_sign_ed25519_sk_to_curve25519(t.privateKey)}}catch(e){throw new Error("Encryption/decryption key derivation failed.",{cause:e})}}function lockData(e,t,{outputFormat:r="base64"}={}){try{let n=null==e?null:e instanceof ArrayBuffer?new Uint8Array(e):isByteArray(e)?e:u("object"==typeof e?JSON.stringify(e):"string"==typeof e?e:String(e));if(null==e)throw new Error("Non-empty data required.");let a=sodium.crypto_box_seal(n,t.encPK);return["base64","base-64"].includes(r.toLowerCase())?c(a):a}catch(e){throw new Error("Data encryption failed.",{cause:e})}}function unlockData(e,t,{outputFormat:r="utf8",parseJSON:n=!0}={}){try{let a=sodium.crypto_box_seal_open("string"==typeof e?l(e):e,t.encPK,t.encSK);if(["utf8","utf-8"].includes(r.toLowerCase())){let e=y(a);return n?JSON.parse(e):e}return a}catch(e){throw new Error("Data decryption failed.",{cause:e})}}function storeLocalIdentities(){var e=Object.fromEntries(Object.entries(k).map((([e,t])=>[e,{...t,passkeys:t.passkeys.map((e=>({...e,publicKey:o(e.publicKey)})))}])));Object.keys(e).length>0?window.localStorage.setItem("local-identities",JSON.stringify(e)):window.localStorage.removeItem("local-identities")}function setMaxLockKeyCacheLifetime(e=18e5){return f=Math.max(0,Number(e)||0)}function isByteArray(e){return e instanceof Uint8Array&&e.buffer instanceof ArrayBuffer}function computePasskeyEntryHash(e){let{hash:t,...r}=e;return c(sodium.crypto_hash(JSON.stringify({...r,publicKey:o(r.publicKey)})))} | ||
import{supportsWebAuthn as e,regDefaults as t,register as r,authDefaults as n,auth as o,verifyAuthResponse as i,packPublicKeyJSON as a,unpackPublicKeyJSON as c,toBase64String as s,fromBase64String as l,toUTF8String as y,fromUTF8String as u,resetAbortReason as p}from"@lo-fi/webauthn-local-client";const d=1,f=sodium.crypto_sign_SEEDBYTES;var k=setMaxLockKeyCacheLifetime(),K=function loadLocalIdentities(){return Object.fromEntries(Object.entries(JSON.parse(window.localStorage.getItem("local-identities")||null)||{}).filter((([e,t])=>"number"==typeof t.lastSeq&&Array.isArray(t.passkeys)&&t.passkeys.length>0&&t.passkeys.every((e=>"string"==typeof e.credentialID&&""!=e.credentialID&&"number"==typeof e.seq&&null!=e.publicKey&&"object"==typeof e.publicKey&&"number"==typeof e.publicKey.algoCOSE&&"string"==typeof e.publicKey.raw&&""!=e.publicKey.raw&&"string"==typeof e.publicKey.spki&&""!=e.publicKey.spki&&"string"==typeof e.hash&&""!=e.hash&&e.hash==computePasskeyEntryHash(e))))).map((([e,t])=>[e,{...t,passkeys:t.passkeys.map((e=>({...e,publicKey:c(e.publicKey)})))}])))}(),b={},h=null;export{e as supportsWebAuthn,a as packPublicKeyJSON,c as unpackPublicKeyJSON,s as toBase64String,l as fromBase64String,y as toUTF8String,u as fromUTF8String,listLocalIdentities,clearLockKeyCache,removeLocalAccount,getLockKey,generateEntropy,deriveLockKey,lockData,unlockData,setMaxLockKeyCacheLifetime};var L={supportsWebAuthn:e,packPublicKeyJSON:a,unpackPublicKeyJSON:c,toBase64String:s,fromBase64String:l,toUTF8String:y,fromUTF8String:u,listLocalIdentities:listLocalIdentities,clearLockKeyCache:clearLockKeyCache,removeLocalAccount:removeLocalAccount,getLockKey:getLockKey,generateEntropy:generateEntropy,deriveLockKey:deriveLockKey,lockData:lockData,unlockData:unlockData,setMaxLockKeyCacheLifetime:setMaxLockKeyCacheLifetime};export default L;function listLocalIdentities(){return Object.keys(K)}function cacheLockKey(e,t,r=!1){e in b&&!r||(b[e]={...t,timestamp:Date.now()})}function clearLockKeyCache(e){null!=e?delete b[e]:b={}}function removeLocalAccount(e){delete b[e],delete K[e],storeLocalIdentities()}async function getLockKey({localIdentity:e=s(generateEntropy(15)),username:a="local-user",displayName:c="Local User",relyingPartyID:l=document.location.hostname,relyingPartyName:y="Local Data Lock",addNewPasskey:u=!1,resetLockKey:p=!1,useLockKey:d=null,verify:L=!0}={}){var m=null!=e?K[e]:null;if(null!=m){let t=function getCachedLockKey(e){if(e in b&&b[e].timestamp>=Date.now()-k){let{timestamp:t,...r}=b[e];return r}}(e);if(null==t||p){if(delete b[e],p)return resetAbortToken(),({record:K[e],lockKey:t}=await registerLocalIdentity(d&&"object"==typeof d?checkLockKey(d):void 0)),storeLocalIdentities(),cacheLockKey(e,t),{...t,localIdentity:e};if(u)throw new Error("Encryption/Decryption key not currently cached, unavailable for new passkey");{resetAbortToken();let t=n({relyingPartyID:l,mediation:"optional",allowCredentials:m.passkeys.map((({credentialID:e})=>({type:"public-key",id:e}))),signal:h.signal}),r=await o(t);if(L){let e=m.passkeys.find((e=>e.credentialID==r.response.credentialID)),t=null!=e?e.publicKey:null;if(!(null!=t&&await i(r.response,t)))throw new Error("Auth verification failed")}return{...extractLockKey(r),localIdentity:e}}}if(u){resetAbortToken();let{record:e}=await registerLocalIdentity(t);m.lastSeq=e.lastSeq,m.passkeys=[...m.passkeys,...e.passkeys],storeLocalIdentities()}return{...t,localIdentity:e}}if(u){resetAbortToken();let{record:t,lockKey:r}=await registerLocalIdentity(d&&"object"==typeof d?checkLockKey(d):void 0);return K[e]=t,cacheLockKey(e,r),storeLocalIdentities(),{...r,localIdentity:e}}{resetAbortToken();let t=n({relyingPartyID:l,mediation:"optional",signal:h.signal}),r=await o(t),a=extractLockKey(r),[c]=Object.entries(K).find((([,e])=>null!=e.passkeys.find((e=>e.credentialID==r.response.credentialID))))||[];if(null!=c){if(delete b[e],m=K[e=c],L){let e=m.passkeys.find((e=>e.credentialID==r.response.credentialID)),t=null!=e?e.publicKey:null;if(!(null!=t&&await i(r.response,t)))throw new Error("Auth verification failed")}cacheLockKey(e,a)}else if(L)throw new Error("Auth verification requested but skipped, against unrecognized passkey (no matching local-identity)");return{...a,localIdentity:e}}async function registerLocalIdentity(n=deriveLockKey()){try{let i=((K[e]||{}).lastSeq||0)+1,s=new Uint8Array(n.iv.byteLength+2),u=new DataView(new ArrayBuffer(2));u.setInt16(0,i,!1),s.set(n.iv,0),s.set(new Uint8Array(u.buffer),n.iv.byteLength);let p=t({relyingPartyID:l,relyingPartyName:y,user:{id:s,name:a,displayName:c},signal:h.signal}),d=await r(p);return{record:{lastSeq:i,passkeys:[(o={seq:i,credentialID:d.response.credentialID,publicKey:d.response.publicKey},{...o,hash:computePasskeyEntryHash(o)})]},lockKey:n}}catch(e){throw new Error("Identity/Passkey registration failed",{cause:e})}var o}function extractLockKey(t){try{if(t&&t.response&&isByteArray(t.response.userID)&&t.response.userID.byteLength==f+2){let r=deriveLockKey(t.response.userID.subarray(0,f));return cacheLockKey(e,r),r}throw new Error("Passkey info missing")}catch(e){throw new Error("Chosen passkey did not provide a valid encryption/decryption key",{cause:e})}}}function resetAbortToken(){h&&h.abort("Passkey operation abandoned."),h=new AbortController}function generateEntropy(e=16){return sodium.randombytes_buf(e)}function deriveLockKey(e=generateEntropy(f)){try{let t=sodium.crypto_sign_seed_keypair(e);return{keyFormatVersion:d,iv:e,publicKey:t.publicKey,privateKey:t.privateKey,encPK:sodium.crypto_sign_ed25519_pk_to_curve25519(t.publicKey),encSK:sodium.crypto_sign_ed25519_sk_to_curve25519(t.privateKey)}}catch(e){throw new Error("Encryption/decryption key derivation failed.",{cause:e})}}function checkLockKey(e){if(e&&"object"==typeof e){if(e.keyFormatVersion===d)return e;if(isByteArray(e.iv)&&e.iv.byteLength==f)return deriveLockKey(e.iv)}throw new Error("Unrecongnized lock-key")}function lockData(e,t,{outputFormat:r="base64"}={}){try{let n=null==e?null:e instanceof ArrayBuffer?new Uint8Array(e):isByteArray(e)?e:u("object"==typeof e?JSON.stringify(e):"string"==typeof e?e:String(e));if(null==e)throw new Error("Non-empty data required.");let o=sodium.crypto_box_seal(n,t.encPK);return["base64","base-64"].includes(r.toLowerCase())?s(o):o}catch(e){throw new Error("Data encryption failed.",{cause:e})}}function unlockData(e,t,{outputFormat:r="utf8",parseJSON:n=!0}={}){try{let o=sodium.crypto_box_seal_open("string"==typeof e?l(e):e,t.encPK,t.encSK);if(["utf8","utf-8"].includes(r.toLowerCase())){let e=y(o);return n?JSON.parse(e):e}return o}catch(e){throw new Error("Data decryption failed.",{cause:e})}}function storeLocalIdentities(){var e=Object.fromEntries(Object.entries(K).map((([e,t])=>[e,{...t,passkeys:t.passkeys.map((e=>({...e,publicKey:a(e.publicKey)})))}])));Object.keys(e).length>0?window.localStorage.setItem("local-identities",JSON.stringify(e)):window.localStorage.removeItem("local-identities")}function setMaxLockKeyCacheLifetime(e=18e5){return k=Math.max(0,Number(e)||0)}function isByteArray(e){return e instanceof Uint8Array&&e.buffer instanceof ArrayBuffer}function computePasskeyEntryHash(e){let{hash:t,...r}=e;return s(sodium.crypto_hash(JSON.stringify({...r,publicKey:a(r.publicKey)})))} |
{ | ||
"name": "@lo-fi/local-data-lock", | ||
"description": "Protect local-first app data with encryption/decryption key secured in Webauthn (biometric) passkeys", | ||
"version": "0.9.5", | ||
"version": "0.10.0", | ||
"exports": { | ||
@@ -6,0 +6,0 @@ ".": "./dist/bundlers/ldl.mjs", |
123
README.md
@@ -6,4 +6,14 @@ # Local Data Lock | ||
**Local Data Lock** provides a simple utility interface for encrypting and decrypting local-first application data using a keypair stored and protected by [Webauthn](https://developer.mozilla.org/en-US/docs/Web/API/Web_Authentication_API) (biometric passkeys). | ||
**Local Data Lock** provides a simple utility interface for encrypting and decrypting local-first application data using a keypair stored and protected by [Webauthn](https://developer.mozilla.org/en-US/docs/Web/API/Web_Authentication_API) (biometric passkeys), via the [**Webauthn Local Client** library](https://github.com/mylofi/webauthn-local-client) -- no servers required! | ||
```js | ||
var lockKey = await getLockKey({ .. }); | ||
var encData = await lockData({ hello: "World!" },lockKey); | ||
// "aG4/z..." | ||
await unlockData(encData,lockKey); | ||
// { hello: "World!" } | ||
``` | ||
---- | ||
@@ -15,16 +25,28 @@ | ||
The intent of this library is to store encrypted data on the device, and protect the encryption/decryption keypair securely in a passkey that the user can access by presenting their biometric factor(s). Further, the cryptographic keypair may also be used for secured asymmetric data transmission. | ||
## Overview | ||
The primary dependency of this library is [**WebAuthn-Local-Client**](https://github.com/mylofi/webauthn-local-client), which wraps the [WebAuthn API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Authentication_API) for managing passkeys entirely in the local client (zero servers). | ||
This library can securely lock (encrypt) data in the local client, with no servers needed. The encrypted data *might also be* stored locally on the client device; for this purpose, please strongly consider using the [**Local Vault** library](https://github.com/mylofi/local-vault). | ||
**Local Data Lock** generates an encryption/decryption keypair, storing that securely in the passkey (via its `userHandle` field), which is protected by the authenticator/device. The library also stores entries for these passkeys in the device's [`LocalStorage`](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage) -- specifically, the public-key info for the passkey itself, which is necessary for **verifying** subsequent passkey authentication responses. | ||
However, the encrypted data (by default, represented as a base64 encoded string) might be transmitted and stored elsewhere, such as on an app's servers. The cryptographic keypair may be used for digital signatures to verify secure data transmission. | ||
**NOTE:** This public-key for a passkey is *NOT* in any way related to the encryption/decryption keypair, which **Local Data Lock** does not persist anywhere on the device (only kept in memory). It's *only* used for authentication verification (protecting against MitM attacks on the device biometric system). Verification defaults to on, but can be skipped by passing `verify: false` as an option to the `getLockKey()` method. | ||
This cryptographic keypair is protected locally on the user's device in a biometric passkey; the user can easily unlock (decrypt) their data, or verify a received data transmission from their other device, by presenting a biometric factor to retrieve the keypair. | ||
Your application accesses the encryption/decryption keypair via `getLockKey()`, and may optionally decide if you want to persist it somewhere -- for more convenience/ease-of-use, as compared to asking the user to re-authenticate their passkey on each usage. But you are cautioned to be very careful in such decisions, striking an appropriate balance between security and convenience. | ||
### How does it work? | ||
To assist in making these difficult tradeoffs, **Local Data Lock** internally caches the encryption/decryption key after a successful passkey authentication, and keeps it in memory (assuming no page refresh) for a period of time (by default, 30 minutes); a user won't need to re-authenticate their passkey more often than once per 30 minutes. This default time threshold can also be adjusted from 0ms or higher, using the `setMaxLockKeyCacheLifetime()` method. | ||
The direct dependency of this library is [**WebAuthn-Local-Client**](https://github.com/mylofi/webauthn-local-client), which utilizes the browser's [WebAuthn API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Authentication_API) for managing biometric passkeys entirely in the local client (zero servers). | ||
You are strongly encouraged **NOT** to persist the encryption/decryption key, and to utilize this time-based caching mechanism. | ||
The cryptographic keypair the library generates, is attached securely to a passkey (via its `userHandle` field), which is protected by the authenticator/device. The library also stores meta-data entries for these passkeys in the device's [`LocalStorage`](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage) -- specifically, the public-key info for the passkey itself, which is necessary for **verifying** subsequent passkey authentication responses. | ||
**NOTE:** This public-key for a passkey is *NOT* in any way related to the crytographic keypair, which **Local Data Lock** does not persist anywhere on the device (only kept in memory). It's *only* used for authentication verification -- protecting against MitM attacks against the authenticator. Verification defaults *on*, but can be skipped by passing `verify: false` as an option to the `getLockKey()` method. | ||
### Security vs Convenience | ||
Your application accesses the cryptographic keypair via `getLockKey()`, and may optionally decide if you want to persist it somewhere -- for more convenience/ease-of-use, as compared to asking the user to re-authenticate their passkey on each usage. But you are cautioned to be very careful in such decisions, striking an appropriate balance between security and convenience. | ||
If the design is *too convenient* (e.g., once-forever logins), it's likely to be insecure (and the user may not realize it!). If the design is *too secure*, it's likely to have so much UX friction that users won't use it (or your app). | ||
To assist in making these difficult tradeoffs, **Local Data Lock** internally caches the cryptographic keypair after a successful passkey authentication, and keeps it in memory (assuming no page refresh) for a period of time (by default, 30 minutes); in such a setup, the user won't need to re-authenticate their passkey more often than once per 30 minutes. This default time threshold can also be adjusted, from 0ms upward, using the `setMaxLockKeyCacheLifetime()` method. | ||
You are strongly encouraged **NOT** to persist the encryption/decryption key, and to instead rely on this time-based caching mechanism. | ||
## Deployment / Import | ||
@@ -60,3 +82,3 @@ | ||
## Registering a local account | ||
## Registering a local account (and lock-key keypair) | ||
@@ -79,2 +101,45 @@ A "local account" is merely a collection of one or more passkeys that are all holding the same encryption/decryption keypair. There's no limit on the number of "local account" passkey collections on a device (other than device storage limits). | ||
### Lock-Key Value Format | ||
Other than reading the `localIdentity` property, the lock-key object should be **treated opaquely**, meaning that you don't rely on its structure, don't make any changes to it, etc. | ||
It contains binary data for the keypairs, in the form of various `Uint8Array` values. These types of data are not, as-is, particularly serialization safe (JSON, etc), for the purposes of storage or transmission. To serialize these binary-array values (and unserialize them later), you can use the `toBase64String()` / `fromBase64String()` utilities exported on the library's API. | ||
For example, to serialize a lock-key for JSON-safe storage, or transmission: | ||
```js | ||
var serializedKey = Object.fromEntries( | ||
Object.entries(key) | ||
.map(([ prop, value ]) => [ | ||
prop, | ||
( | ||
value instanceof Uint8Array && | ||
value.buffer instanceof ArrayBuffer | ||
) ? | ||
toBase64String(value) : | ||
value | ||
]) | ||
); | ||
``` | ||
And to deserialize: | ||
```js | ||
var key = Object.fromEntries( | ||
Object.entries(serializedKey) | ||
.map(([ prop, value ]) => [ | ||
prop, | ||
( | ||
typeof value == "string" && | ||
// padded base64 encoding of Uint8Array(32) | ||
// will be at least 44 characters long | ||
value.length >= 44 | ||
) ? | ||
fromBase64String(value) : | ||
value | ||
]) | ||
); | ||
``` | ||
### Obtaining the keypair from existing account/passkey | ||
@@ -231,3 +296,3 @@ | ||
This keypair is suitable to use with `lockData()` and `unlockData()` methods. However, the keypair returned WILL NOT be associated with (or protected by) a device passkey; it receives no entry in the device's local-storage and will never be returned from `getLockKey()`. The intent of this library is to rely on passkeys, so you are encouraged *not* to pursue this manual approach unless strictly necessary. | ||
This keypair is suitable to use with `lockData()` and `unlockData()` methods. However, the keypair returned WILL NOT be associated with (or protected by) a device passkey; it receives no entry in the device's local-storage and will not be returned from `getLockKey()`. The intent of this library is to rely on passkeys, so you are encouraged *not* to pursue this manual approach unless strictly necessary. | ||
@@ -246,2 +311,29 @@ Further, to generate a suitable cryptograhpically random `seedValue`: | ||
## Importing an encryption/decryption key | ||
If you have a lock-key keypair generated by **Local Vault** / **Local Data Lock**, either from manually calling [`deriveLockKey()`](#deriving-an-encryptiondecryption-key), or from a previous call to `getLockKey()` (even on another device!), you *can* choose to import it to a local account. | ||
When registering a new local-account: | ||
```js | ||
var key = await getLockKey({ | ||
addNewPasskey: true, | ||
useLockKey: existingLockKey, | ||
}); | ||
key === existingLockKey; // true | ||
``` | ||
When resetting the key on an existing local-account: | ||
```js | ||
var key = await getLockKey({ | ||
localIdentitity: currentAccountID, | ||
resetLockKey: true, | ||
useLockKey: existingLockKey, | ||
}); | ||
key === existingLockKey; // true | ||
``` | ||
**Warning:** You should generally let **Local Data Lock** internally generate and manage the lock-keys on local-accounts, and should not store (or transmit) these lock-keys in a way that degrades the security promises of this library. Be very careful if you are using the library in a way that you need to use `useLockKey`, and make sure it's absolutely necessary. | ||
## WebAuthn-Local-Client Utilities | ||
@@ -278,13 +370,12 @@ | ||
To locally run the tests, start the simple static server (no server-side logic): | ||
To instead run the tests locally, first make sure you've [already run the build](#re-building-dist), then: | ||
```cmd | ||
# only needed one time | ||
npm install | ||
npm run test:start | ||
npm test | ||
``` | ||
Then visit `http://localhost:8080/` in a browser. | ||
This will start a static file webserver (no server logic), serving the interactive test page from `http://localhost:8080/`; visit this page in your browser to perform tests. | ||
By default, the `test/test.js` file imports the code from the `src/*` directly. However, to test against the `dist/auto/*` files (as included in the npm package), you can modify `test/test.js`, updating the `/src` in its `import` statement to `/dist` (see the import-map in `test/index.html` for more details). | ||
## License | ||
@@ -291,0 +382,0 @@ |
@@ -20,2 +20,3 @@ import { | ||
const CURRENT_LOCK_KEY_FORMAT_VERSION = 1; | ||
const IV_BYTE_LENGTH = sodium.crypto_sign_SEEDBYTES; | ||
@@ -127,5 +128,5 @@ var MAX_LOCK_KEY_CACHE_LIFETIME = setMaxLockKeyCacheLifetime(); | ||
relyingPartyName = "Local Data Lock", | ||
addNewPasskey = false, | ||
resetLockKey = false, | ||
useLockKey = null, | ||
verify = true, | ||
@@ -162,3 +163,3 @@ } = {}, | ||
// create new lock-key (and passkey)? | ||
// create (or import) new lock-key (and passkey)? | ||
if (resetLockKey) { | ||
@@ -173,4 +174,10 @@ resetAbortToken(); | ||
lockKey, | ||
} = await registerLocalIdentity()); | ||
} = await registerLocalIdentity( | ||
// manually importing an external lock-key? | ||
useLockKey && typeof useLockKey == "object" ? | ||
checkLockKey(useLockKey) : | ||
undefined | ||
)); | ||
storeLocalIdentities(); | ||
cacheLockKey(localID,lockKey); | ||
@@ -280,3 +287,8 @@ | ||
resetAbortToken(); | ||
let { record, lockKey, } = await registerLocalIdentity(); | ||
let { record, lockKey, } = await registerLocalIdentity( | ||
// manually importing an external lock-key? | ||
useLockKey && typeof useLockKey == "object" ? | ||
checkLockKey(useLockKey) : | ||
undefined | ||
); | ||
localIdentities[localID] = record; | ||
@@ -338,3 +350,2 @@ cacheLockKey(localID,lockKey); | ||
catch (err) { | ||
console.log(err); | ||
throw new Error("Identity/Passkey registration failed",{ cause: err, }); | ||
@@ -384,2 +395,3 @@ } | ||
return { | ||
keyFormatVersion: CURRENT_LOCK_KEY_FORMAT_VERSION, | ||
iv, | ||
@@ -401,2 +413,22 @@ publicKey: ed25519KeyPair.publicKey, | ||
function checkLockKey(lockKeyCandidate) { | ||
if ( | ||
lockKeyCandidate && | ||
typeof lockKeyCandidate == "object" | ||
) { | ||
// assume current format key? | ||
if (lockKeyCandidate.keyFormatVersion === CURRENT_LOCK_KEY_FORMAT_VERSION) { | ||
return lockKeyCandidate; | ||
} | ||
// contains a suitable `iv` we can derive from? | ||
else if ( | ||
isByteArray(lockKeyCandidate.iv) && | ||
lockKeyCandidate.iv.byteLength == IV_BYTE_LENGTH | ||
) { | ||
return deriveLockKey(lockKeyCandidate.iv); | ||
} | ||
} | ||
throw new Error("Unrecongnized lock-key"); | ||
} | ||
function lockData( | ||
@@ -403,0 +435,0 @@ data, |
Sorry, the diff of this file is not supported yet
858162
4648
379