@lo-fi/local-vault
Advanced tools
Comparing version 0.9.4 to 0.10.0
/*! Local-Vault: adapter.cookie.js | ||
v0.9.4 (c) 2024 Kyle Simpson | ||
v0.10.0 (c) 2024 Kyle Simpson | ||
MIT License: http://getify.mit-license.org | ||
@@ -4,0 +4,0 @@ */ |
/*! Local-Vault: adapter.idb.js | ||
v0.9.4 (c) 2024 Kyle Simpson | ||
v0.10.0 (c) 2024 Kyle Simpson | ||
MIT License: http://getify.mit-license.org | ||
@@ -4,0 +4,0 @@ */ |
/*! Local-Vault: adapter.local-storage.js | ||
v0.9.4 (c) 2024 Kyle Simpson | ||
v0.10.0 (c) 2024 Kyle Simpson | ||
MIT License: http://getify.mit-license.org | ||
@@ -4,0 +4,0 @@ */ |
/*! Local-Vault: adapter.opfs.js | ||
v0.9.4 (c) 2024 Kyle Simpson | ||
v0.10.0 (c) 2024 Kyle Simpson | ||
MIT License: http://getify.mit-license.org | ||
@@ -4,0 +4,0 @@ */ |
/*! Local-Vault: adapter.session-storage.js | ||
v0.9.4 (c) 2024 Kyle Simpson | ||
v0.10.0 (c) 2024 Kyle Simpson | ||
MIT License: http://getify.mit-license.org | ||
@@ -4,0 +4,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)})))} |
/*! Local-Vault: lv.js | ||
v0.9.4 (c) 2024 Kyle Simpson | ||
v0.10.0 (c) 2024 Kyle Simpson | ||
MIT License: http://getify.mit-license.org | ||
*/ | ||
import{supportsWebAuthn as t,toBase64String as e,fromBase64String as a,getLockKey as r,lockData as n,unlockData as l,generateEntropy as o,listLocalIdentities as i,removeLocalAccount as c,clearLockKeyCache as u}from"@lo-fi/local-data-lock";var y=null,s={},d={},p=new WeakMap;export{t as supportsWebAuthn,i as listLocalIdentities,c as removeLocalAccount,e as toBase64String,a as fromBase64String,defineAdapter,connect,removeAll,keepStorage};var g={supportsWebAuthn:t,listLocalIdentities:i,removeLocalAccount:c,toBase64String:e,fromBase64String:a,defineAdapter:defineAdapter,connect:connect,removeAll:removeAll,keepStorage:keepStorage};export default g;function defineAdapter({storageType:t="unknown",read:e,write:a,find:r,clear:n}){if(t in s)throw new Error(`Storage type ('${t}') already defined`);s[t]={read:e,write:a,find:r,clear:n}}async function connect({storageType:t,vaultID:a,keyOptions:{relyingPartyID:l=document.location.hostname,relyingPartyName:c="Local Vault",localIdentity:u,...y}={},addNewVault:g=!1,discoverVault:w=!1}){if(t in s){if(w){let e=i(),a=await r({relyingPartyID:l,relyingPartyName:c,...y});if(e.includes(a.localIdentity)){let[e]=await s[t].find({accountID:a.localIdentity});if(null!=e)return connect({storageType:t,vaultID:e,keyOptions:{relyingPartyID:l,relyingPartyName:c,...y}})}throw new Error(`No matching vault found in storage ('${t}') for presented passkey`)}if(null!=a||g){if(g&&null==a&&(a=e(o(12)).replace(/[^a-zA-Z0-9]+/g,"")),!(a in d)){d[a]={has:has,get:get,set:set,remove:remove,clear:clear,lock:lock,addPasskey:addPasskey,resetLockKey:resetLockKey,keys:keys,entries:entries};for(let[t,e]of Object.entries(d[a]))d[a][t]=e.bind(d[a]);Object.defineProperties(d[a],{storageType:{value:t,writable:!1,configurable:!1,enumerable:!0},id:{value:a,writable:!1,configurable:!1,enumerable:!0}})}let i=await getVaultEntry(t,a),w=g||null!=i.accountID?await r({relyingPartyID:null!=i.rpID?i.rpID:l,relyingPartyName:c,...y,...g?{addNewPasskey:!0,localIdentity:u}:{localIdentity:i.accountID}}):null;if(null!=w&&null!=w.localIdentity)return unlockVaultEntry(i,w),i.accountID==w.localIdentity&&i.rpID==l||(i.accountID=w.localIdentity,i.rpID=l,await s[t].write(a,i,n(i.data,w))),d[a];throw p.delete(d[a]),delete d[a],new Error("Vault lock-key access failed")}throw new Error("Required vault ID missing")}throw new Error(`Unknown storage type ('${t}')`)}async function removeAll(){for(let[t,e]of Object.entries(s))try{await e.clear()}catch(t){}return!0}async function has(t){var{vaultEntry:e}=await openVault(this);return t in e.data}async function get(t){var{vaultEntry:e}=await openVault(this);return e.data[t]}async function set(t,e){if(void 0===e)return remove(t);var{storageType:a,vaultID:r,vaultEntry:l,vaultLockKey:o}=await openVault(this);return l.data[t]=e,await s[a].write(r,l,n(l.data,o)),!0}async function remove(t){var{storageType:e,vaultID:a,vaultEntry:r,vaultLockKey:l}=await openVault(this);return delete r.data[t],await s[e].write(a,r,n(r.data,l)),!0}async function clear(){var{storageType:t,vaultID:e}=await openVault(this);return await s[t].clear(e),p.delete(d[e]),!0}function lock(){var t=this;if(null!=t&&"string"==typeof t.storageType&&"string"==typeof t.id&&p.has(t)){let e=p.get(t);return p.delete(t),u(e.accountID),!0}throw new Error("Not a currently unlocked vault")}async function addPasskey({username:t,displayName:e}={}){var{vaultEntry:a}=await openVault(this);try{return await r({localIdentity:a.accountID,username:t,displayName:e,relyingPartyID:a.rpID,relyingPartyName:"Local Vault",addNewPasskey:!0}),!0}catch(t){throw new Error("Adding passkey to vault's local-account failed",{cause:t})}}async function resetLockKey({username:t,displayName:e}={}){var{storageType:a,vaultID:l,vaultEntry:o,vaultLockKey:i}=await openVault(this);try{let i=await r({localIdentity:o.accountID,username:t,displayName:e,relyingPartyID:o.rpID,relyingPartyName:"Local Vault",resetLockKey:!0});return await s[a].write(l,o,n(o.data,i)),!0}catch(t){throw new Error("Resetting vault's lock-key failed",{cause:t})}}async function keys(){var{vaultEntry:t}=await openVault(this);return Object.keys(t.data)}async function entries(){var{vaultEntry:t}=await openVault(this);return Object.entries(t.data)}async function openVault(t){if(null!=t&&"string"==typeof t.storageType&&"string"==typeof t.id){let{storageType:e,id:a}=t;if(null!=s[e]){let t=await getVaultEntry(e,a),n=await r({localIdentity:t.accountID,relyingPartyID:t.rpID});return unlockVaultEntry(t,n),{storageType:e,vaultID:a,vaultEntry:t,vaultLockKey:n}}throw new Error(`Unknown storage type ('${e}')`)}throw new Error("Unrecognized vault instance")}async function getVaultEntry(t,e){var a=p.has(d[e])?p.get(d[e]):await s[t].read(e);return p.set(d[e],a),a}function unlockVaultEntry(t,e){"string"==typeof t.data&&(t.data=t.data.length>0?l(t.data,e):{})}async function keepStorage(){if(null==y)try{if(y=await navigator.storage.persisted())return y;y=await navigator.storage.persist()}catch(t){y=!1}return y} | ||
import{supportsWebAuthn as t,toBase64String as e,fromBase64String as a,getLockKey as r,lockData as n,unlockData as l,generateEntropy as o,listLocalIdentities as i,removeLocalAccount as c,clearLockKeyCache as u}from"@lo-fi/local-data-lock";var y=null,s={},d={},g=new WeakMap;export{t as supportsWebAuthn,i as listLocalIdentities,c as removeLocalAccount,e as toBase64String,a as fromBase64String,defineAdapter,connect,removeAll,keepStorage};var p={supportsWebAuthn:t,listLocalIdentities:i,removeLocalAccount:c,toBase64String:e,fromBase64String:a,defineAdapter:defineAdapter,connect:connect,removeAll:removeAll,keepStorage:keepStorage};export default p;function defineAdapter({storageType:t="unknown",read:e,write:a,find:r,clear:n}){if(t in s)throw new Error(`Storage type ('${t}') already defined`);s[t]={read:e,write:a,find:r,clear:n}}async function connect({storageType:t,vaultID:a,keyOptions:{relyingPartyID:l=document.location.hostname,relyingPartyName:c="Local Vault",localIdentity:u,...y}={},addNewVault:p=!1,discoverVault:f=!1}){if(t in s){if(f){let e=i(),a=await r({...y,relyingPartyID:l,relyingPartyName:c});if(e.includes(a.localIdentity)){let[e]=await s[t].find({accountID:a.localIdentity});if(null!=e)return connect({storageType:t,vaultID:e,keyOptions:{relyingPartyID:l,relyingPartyName:c,...y}})}throw new Error(`No matching vault found in storage ('${t}') for presented passkey`)}if(null!=a||p){if(p&&null==a&&(a=e(o(12)).replace(/[^a-zA-Z0-9]+/g,"")),!(a in d)){d[a]={has:has,get:get,set:set,remove:remove,clear:clear,lock:lock,addPasskey:addPasskey,resetLockKey:resetLockKey,keys:keys,entries:entries,__exportLockKey:__exportLockKey};for(let[t,e]of Object.entries(d[a]))d[a][t]=e.bind(d[a]);Object.defineProperties(d[a],{storageType:{value:t,writable:!1,configurable:!1,enumerable:!0},id:{value:a,writable:!1,configurable:!1,enumerable:!0}})}let i=await getVaultEntry(t,a),f=p||null!=i.accountID?await r({...y,relyingPartyID:null!=i.rpID?i.rpID:l,relyingPartyName:c,...p?{addNewPasskey:!0,localIdentity:u}:{localIdentity:i.accountID}}):null;if(null!=f&&null!=f.localIdentity)return unlockVaultEntry(i,f),i.accountID==f.localIdentity&&i.rpID==l||(i.accountID=f.localIdentity,i.rpID=l,await s[t].write(a,i,n(i.data,f))),d[a];throw g.delete(d[a]),delete d[a],new Error("Vault lock-key access failed")}throw new Error("Required vault ID missing")}throw new Error(`Unknown storage type ('${t}')`)}async function removeAll(){for(let[t,e]of Object.entries(s))try{await e.clear()}catch(t){}return!0}async function has(t){var{vaultEntry:e}=await openVault(this);return t in e.data}async function get(t){var{vaultEntry:e}=await openVault(this);return e.data[t]}async function set(t,e){if(void 0===e)return remove(t);var{storageType:a,vaultID:r,vaultEntry:l,vaultLockKey:o}=await openVault(this);return l.data[t]=e,await s[a].write(r,l,n(l.data,o)),!0}async function remove(t){var{storageType:e,vaultID:a,vaultEntry:r,vaultLockKey:l}=await openVault(this);return delete r.data[t],await s[e].write(a,r,n(r.data,l)),!0}async function clear(){var{storageType:t,vaultID:e}=await openVault(this);return await s[t].clear(e),g.delete(d[e]),!0}function lock(){var t=this;if(null!=t&&"string"==typeof t.storageType&&"string"==typeof t.id&&g.has(t)){let e=g.get(t);return g.delete(t),u(e.accountID),!0}throw new Error("Not a currently unlocked vault")}async function addPasskey({localIdentity:t,relyingPartyID:e=document.location.hostname,relyingPartyName:a="Local Vault",...n}={}){var{vaultEntry:l}=await openVault(this);try{return await r({...n,localIdentity:l.accountID,relyingPartyID:null!=l.rpID?l.rpID:e,relyingPartyName:a,addNewPasskey:!0}),!0}catch(t){throw new Error("Adding passkey to vault's local-account failed",{cause:t})}}async function resetLockKey({localIdentity:t,relyingPartyID:e=document.location.hostname,relyingPartyName:a="Local Vault",...l}={}){var{storageType:o,vaultID:i,vaultEntry:c}=await openVault(this);try{let t=await r({...l,localIdentity:c.accountID,relyingPartyID:null!=c.rpID?c.rpID:e,relyingPartyName:a,resetLockKey:!0});return await s[o].write(i,c,n(c.data,t)),!0}catch(t){throw new Error("Resetting vault's lock-key failed",{cause:t})}}async function keys(){var{vaultEntry:t}=await openVault(this);return Object.keys(t.data)}async function entries(){var{vaultEntry:t}=await openVault(this);return Object.entries(t.data)}async function __exportLockKey({risky:t=!1}={}){if("this is unsafe"==t){let{vaultLockKey:t}=await openVault(this);return{...t}}throw new Error('Must pass {risky:"this is unsafe"} argument, to acknowledge the risks of using this method')}async function openVault(t){if(null!=t&&"string"==typeof t.storageType&&"string"==typeof t.id){let{storageType:e,id:a}=t;if(null!=s[e]){let t=await getVaultEntry(e,a),n=await r({localIdentity:t.accountID,relyingPartyID:t.rpID});return unlockVaultEntry(t,n),{storageType:e,vaultID:a,vaultEntry:t,vaultLockKey:n}}throw new Error(`Unknown storage type ('${e}')`)}throw new Error("Unrecognized vault instance")}async function getVaultEntry(t,e){var a=g.has(d[e])?g.get(d[e]):await s[t].read(e);return g.set(d[e],a),a}function unlockVaultEntry(t,e){"string"==typeof t.data&&(t.data=t.data.length>0?l(t.data,e):{})}async function keepStorage(){if(null==y)try{if(y=await navigator.storage.persisted())return y;y=await navigator.storage.persist()}catch(t){y=!1}return y} |
@@ -14,2 +14,3 @@ # Deploying Local Vault WITHOUT A Bundler | ||
* `dist/auto/external/*` (preserve the whole `external/` sub-directory): | ||
- `idb-keyval.js` | ||
- `@lo-fi/local-data-lock/ldl.js`, | ||
@@ -43,2 +44,4 @@ - `@lo-fi/local-data-lock/external/@lo-fi/webauthn-local-client/walc.js` | ||
"idb-keyval": "/path/to/js-assets/local-vault/external/idb-keyval.js", | ||
"@lo-fi/local-data-lock": "/path/to/js-assets/local-vault/external/@lo-fi/local-data-lock/ldl.js", | ||
@@ -45,0 +48,0 @@ |
{ | ||
"name": "@lo-fi/local-vault", | ||
"description": "Store key-value data encrypted (biometric passkey protected), locally in the client", | ||
"version": "0.9.4", | ||
"version": "0.10.0", | ||
"exports": { | ||
@@ -34,3 +34,3 @@ ".": "./dist/bundlers/lv.mjs", | ||
"dependencies": { | ||
"@lo-fi/local-data-lock": "~0.9.5", | ||
"@lo-fi/local-data-lock": "~0.10.0", | ||
"idb-keyval": "~6.2.1" | ||
@@ -37,0 +37,0 @@ }, |
@@ -6,4 +6,12 @@ # Local Vault | ||
**Local Vault** provides a client-side, key-value storage API abstraction, with automatic encryption/decryption secured by biometric passkeys. | ||
**Local Vault** provides a client-side, key-value storage API abstraction, with automatic encryption/decryption secured by biometric passkeys -- no servers required! | ||
```js | ||
var vault = await connect({ .. }); | ||
await vault.set("Hello","World!"); // true | ||
await vault.get("Hello"); // "World!" | ||
``` | ||
---- | ||
@@ -15,2 +23,4 @@ | ||
## Overview | ||
A *local vault* instance is a simple key-value store (`get()`, `set()`, etc), backed by [your choice among various client-side storage mechanisms](#client-side-storage-adapters) (`localStorage` / `sessionStorage`, IndexedDB, cookies, and OPFS). | ||
@@ -151,3 +161,3 @@ | ||
* `rpID` string, holding the "ID" of the "relying party", which for web applications should almost always be the fully-qualified hostname of the webapp | ||
* `rpID` string, holding the "ID" of the "relying party", which for web applications should almost always be the fully-qualified hostname (i.e., `document.location.hostname`) of the webapp | ||
@@ -184,14 +194,36 @@ * `data` string, holding the encrypted data in base64 encoding | ||
**Note:** The `username` / `displayName` values are only passed along to the biometric passkey, and are only used as meta-data for such. The device will often use one or both values in its prompt dialogs, so these values should either be something the user has picked, or at least be something the user will recognize and trust. Also, there may very well be multiple passkeys associated with the same local account, so the username/display-name should probably be differentiated to help the users know which passkey they're authenticating with. | ||
Any options set under `keyOptions` are passed along to the underlying [**Local Data Lock** library's `getLockKey()` method](https://github.com/mylofi/local-data-lock?tab=readme-ov-file#registering-a-local-account). | ||
Generally, you'll probably save the auto-generated `vault.id` value to use in subsequent reconnections. To reconnect to an existing vault, use `connect()` again: | ||
**Note:** The `username` / `displayName` key-options illustrated above are not strictly required, but are strongly recommended; they're only passed along to the biometric passkey, as meta-data for such. The device will often use one or both values in its prompt dialogs, so these values should either be something the user has picked, or at least be something the user will recognize and trust. Also, there may very well be multiple passkeys associated with the same local account, so the username/display-name should be differentiated to help the users know which passkey they're authenticating with. | ||
### Manually Setting Lock-Key | ||
To manually set a new vault's lock-key -- for example, when importing a lock-key from another device: | ||
```js | ||
var vault = await connect({ | ||
storageType: "idb", | ||
addNewVault: true, | ||
keyOptions: { | ||
// created by another vault instance, even on | ||
// another device | ||
useLockKey: existingLockKey | ||
} | ||
}); | ||
``` | ||
**Warning:** Be judicious with this feature. If for example you're synchronizing keypair identities between a user's multiple devices, this functionality can be useful. But otherwise, it's better (more secure!) to let **Local Vault** automatically generate and internally manage its cryptographic keypair. | ||
## Reconnecting a Vault | ||
You may save the auto-generated `vault.id` value from a `connect()` call, to use in subsequent reconnections, again using `connect()`: | ||
```js | ||
var vault = await connect({ | ||
storageType: "idb", | ||
vaultID: ".." // existing vault ID | ||
vaultID: existingVaultID // saved from previous connect() call | ||
}); | ||
``` | ||
**Note:** If the vault's lock-key (biometric passkey protected) is still in the recent-access cache (default timeout: 30 minutes), the `connect()` call will complete silently. Otherwise, the user will be prompted by the device to re-authenticate with their passkey (to access the lock-key) before unlocking the vault. | ||
**Note:** If the vault's lock-key (biometric passkey protected) is still in the recent-access cache (default timeout: 30 minutes), a `connect()` call will complete silently. Otherwise, the user will be prompted by the device to re-authenticate with their passkey (to access the lock-key) before unlocking the vault. | ||
@@ -213,3 +245,3 @@ ### Discoverable Vaults | ||
**Note:** Discovery mode will *always* prompt the user for passkey authentication. This might mean a user would have to reauthenticate on each page load, for example. As an affordance to reduce user friction, you might choose to store the vault-ID in `sessionStorage`, along with a timestamp. Even after page refreshes, you might keep using this vault-ID **without** discovery mode. But after a certain amount of time since last authentication has passed, you might choose to push for reauthentication by using discovery mode. Alternatively, you might call `lock()` on the vault-instance after a certain period of time, thereby ensuring the next vault operation will re-prompt the user for passkey authentication. | ||
**Note:** Discovery mode will *always* prompt the user for passkey authentication. This might mean a user would have to reauthenticate on each page load, for example. As an affordance to reduce user friction, you might choose to store the vault-ID in `sessionStorage`, along with a timestamp. Even after page refreshes, you might keep using this vault-ID **without** discovery mode. But after a certain amount of time since last authentication has passed, you might choose to push for reauthentication by discarding the known vault-ID and using discovery mode. Alternatively, you might call `lock()` on the vault-instance after a certain period of time, thereby ensuring the next vault operation will re-prompt the user for passkey authentication. | ||
@@ -255,3 +287,3 @@ ## Removing All Local Data | ||
storageType: "idb", | ||
addNewVault: true, | ||
addNewVault: true | ||
}); | ||
@@ -287,4 +319,10 @@ | ||
A vault instance has the following methods: | ||
A vault instance has the following properties: | ||
* `id` (string): the unique (to this device) vault ID | ||
* `storageType` (string): the type of storage mechanism chosen to back this vault | ||
A vault instance also has the following methods: | ||
* `has(key)` (async): checks if the key-value store has the specified property registered | ||
@@ -302,6 +340,8 @@ | ||
* `addPasskey({ username, displayName })` (async): add a new passkey to the vault's associated local passkey account (copying the existing lock-key into the new passkey) | ||
* `addPasskey({ ...keyOptions })` (async): add a new passkey to the vault's associated local passkey account | ||
* `resetLockKey({ username, displayName })` (async): regenerate a new vault lock-key, as well as a new passkey to hold this lock-key; discards references to any previous passkeys in the local account | ||
* `resetLockKey({ ...keyOptions })` (async): regenerate a new vault lock-key, as well as a new passkey to hold this lock-key; discards references to any previous passkeys in the local account. | ||
A `useLockKey` key-option may be passed, to manually set the lock-key to reset the vault with; this may be useful if importing a key from another device. | ||
* `keys()` (async): returns an array of all keys in the key-value store | ||
@@ -311,4 +351,10 @@ | ||
**Note:** All of these methods, except `lock()`, are asynchronous (promise-returning), because they all potentially require passkey re-authentication if the vault's lock-key is not fresh in the recently-used cache. | ||
* `__exportLockKey({ risky: "this is unsafe" })` (async): call this method (with the required `risky` argument as shown) to reveal the underlying lock-key for a vault; needs either an unlocked vault (freshly-available cached passkey authentication), or will prompt the user for re-authentication to unlock. | ||
**Warning:** You should only use this method if you're intentionally circumventing the typical security protections of this library. For example: a valid use-case is for synchronizing a lock-key to another of the user's devices. | ||
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. See ["Lock Key Value Format"](https://github.com/mylofi/local-data-lock?tab=readme-ov-file#lock-key-value-format) for information about serializing/deserializing the value for storage, transmission, etc. | ||
**Note:** All of these methods, except `lock()`, are asynchronous (promise-returning), because they all potentially require passkey re-authentication if the vault's lock-key is not freshly available in the internal recently-used cache. | ||
## Re-building `dist/*` | ||
@@ -333,13 +379,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` statements to `/dist` (see the import-map in `test/index.html` for more details). | ||
## License | ||
@@ -346,0 +391,0 @@ |
@@ -91,5 +91,5 @@ import { | ||
let vaultLockKey = await getLockKey({ | ||
...keyOptions, | ||
relyingPartyID, | ||
relyingPartyName, | ||
...keyOptions, | ||
}); | ||
@@ -139,2 +139,3 @@ | ||
entries, | ||
__exportLockKey, | ||
}; | ||
@@ -170,2 +171,3 @@ for (let [ name, fn ] of Object.entries(vaults[vaultID])) { | ||
await getLockKey({ | ||
...keyOptions, | ||
relyingPartyID: ( | ||
@@ -175,3 +177,2 @@ vaultEntry.rpID != null ? vaultEntry.rpID : relyingPartyID | ||
relyingPartyName, | ||
...keyOptions, | ||
...( | ||
@@ -290,3 +291,8 @@ addNewVault ? | ||
async function addPasskey({ username, displayName, } = {}) { | ||
async function addPasskey({ | ||
localIdentity, | ||
relyingPartyID = document.location.hostname, | ||
relyingPartyName = "Local Vault", | ||
...keyOptions | ||
} = {}) { | ||
var { vaultEntry, } = await openVault(this); | ||
@@ -296,7 +302,8 @@ | ||
await getLockKey({ | ||
...keyOptions, | ||
localIdentity: vaultEntry.accountID, | ||
username, | ||
displayName, | ||
relyingPartyID: vaultEntry.rpID, | ||
relyingPartyName: "Local Vault", | ||
relyingPartyID: ( | ||
vaultEntry.rpID != null ? vaultEntry.rpID : relyingPartyID | ||
), | ||
relyingPartyName, | ||
addNewPasskey: true, | ||
@@ -311,12 +318,18 @@ }); | ||
async function resetLockKey({ username, displayName, } = {}) { | ||
var { storageType, vaultID, vaultEntry, vaultLockKey: oldVaultLockKey } = await openVault(this); | ||
async function resetLockKey({ | ||
localIdentity, | ||
relyingPartyID = document.location.hostname, | ||
relyingPartyName = "Local Vault", | ||
...keyOptions | ||
} = {}) { | ||
var { storageType, vaultID, vaultEntry, } = await openVault(this); | ||
try { | ||
let newVaultLockKey = await getLockKey({ | ||
let vaultLockKey = await getLockKey({ | ||
...keyOptions, | ||
localIdentity: vaultEntry.accountID, | ||
username, | ||
displayName, | ||
relyingPartyID: vaultEntry.rpID, | ||
relyingPartyName: "Local Vault", | ||
relyingPartyID: ( | ||
vaultEntry.rpID != null ? vaultEntry.rpID : relyingPartyID | ||
), | ||
relyingPartyName, | ||
resetLockKey: true, | ||
@@ -328,3 +341,3 @@ }); | ||
vaultEntry, | ||
lockData(vaultEntry.data,newVaultLockKey) | ||
lockData(vaultEntry.data,vaultLockKey) | ||
); | ||
@@ -349,2 +362,12 @@ | ||
async function __exportLockKey({ risky = false, } = {}) { | ||
if (risky == "this is unsafe") { | ||
let { vaultLockKey, } = await openVault(this); | ||
return { ...vaultLockKey, }; | ||
} | ||
else { | ||
throw new Error("Must pass {risky:\"this is unsafe\"} argument, to acknowledge the risks of using this method"); | ||
} | ||
} | ||
async function openVault(vault) { | ||
@@ -351,0 +374,0 @@ if ( |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
899295
5374
385
+ Added@lo-fi/local-data-lock@0.10.0(transitive)
- Removed@lo-fi/local-data-lock@0.9.5(transitive)