const DFIDO2_LIB_LOCALSTG_NAME_USER_SESSION = 'fido2_user_session' const DFIDO2_LIB_LOCALSTG_NAME_REGISTERED = 'dfido2_lib_registered' const DFIDO2_LIB_LOCALSTG_NAME_SVR_URL = 'dfido2_lib_svr_url' /** ===APIs=== */ if(!localStorage.getItem(DFIDO2_LIB_LOCALSTG_NAME_SVR_URL)){ localStorage.setItem(DFIDO2_LIB_LOCALSTG_NAME_SVR_URL, 'https://fido2.amipro.me') } function setFidoServerURL(url){ localStorage.setItem(DFIDO2_LIB_LOCALSTG_NAME_SVR_URL, url); } function canTryAutoAuthentication(){ //const session_text = localStorage.getItem(DFIDO2_LIB_LOCALSTG_NAME_USER_SESSION) //alert('canTryAuth:'+session_text+"|"+(null != localStorage.getItem(DFIDO2_LIB_LOCALSTG_NAME_REGISTERED))) return null != localStorage.getItem(DFIDO2_LIB_LOCALSTG_NAME_REGISTERED) } /** * * @param {String} userId * @param {String} rpId */ async function authenticateFido2(userId = null, rpId = null) { var result result = await doAssertion(userId, rpId); if(result.status === 'ok'){ sessionStorage.setItem(DFIDO2_LIB_LOCALSTG_NAME_USER_SESSION, JSON.stringify({session:result.session, uid:result.username})) localStorage.setItem(DFIDO2_LIB_LOCALSTG_NAME_REGISTERED, new Date()); } return result } /** * * @param {String} userId * @param {String} rpId */ async function registerFido2(userId, userDisplay, rpId = null) { if (isWebAuthnSupported()) { const result = await doAttestation(userId, userDisplay, rpId); if(result.status === 'ok'){ localStorage.setItem(DFIDO2_LIB_LOCALSTG_NAME_REGISTERED, new Date()); sessionStorage.setItem(DFIDO2_LIB_LOCALSTG_NAME_USER_SESSION, JSON.stringify({session:result.session, uid:result.username})) } return result }else return {status:'failed', errorMessage: getI18NErrorMessage('Fido2LibErr101:')} } /** * * @param {String} rpId * @returns */ async function listUserDevicesFido2(rpId = null) { try { const session_text = sessionStorage.getItem(DFIDO2_LIB_LOCALSTG_NAME_USER_SESSION) if(!session_text) return {status:'ok', devices:[]} const session_data = JSON.parse(session_text) let req = {session: session_data.session} if (rpId && 0 < rpId.length) { req.rp = { id: rpId }; } const response = await fetch(localStorage.getItem(DFIDO2_LIB_LOCALSTG_NAME_SVR_URL) + "/usr/dvs/lst", { method: "POST", cache: "no-cache", headers: { "Content-Type": "application/json" }, body: JSON.stringify(req) }); const resp = await response.json(); if ('ok' === resp.status && resp.session === session_data.session) { return {status:'ok', devices:resp.devices} } else { return {status:'failed', errorMessage: resp.errorMessage} } } catch (err) { console.log(err) let msg = err.message ? err.message : err; //console.error("Assertion err: ", err); var errRtn = {status:'failed', errorMessage: msg}; if(err.name) errRtn.name = err.name return errRtn; } } async function delUserDeviceFido2(device_id, rpId = null) { try { const session_text = sessionStorage.getItem(DFIDO2_LIB_LOCALSTG_NAME_USER_SESSION) const session_data = JSON.parse(session_text) let req = {session: session_data.session, device_id: device_id} if (rpId && 0 < rpId.length) { req.rp = { id: rpId }; } const response = await fetch(localStorage.getItem(DFIDO2_LIB_LOCALSTG_NAME_SVR_URL) + "/usr/dvs/rm", { method: "POST", cache: "no-cache", headers: { "Content-Type": "application/json" }, body: JSON.stringify(req) }); const resp = await response.json(); return resp } catch (err) { console.log(err) let msg = err.message ? err.message : err; //console.error("Assertion err: ", err); var errRtn = {status:'failed', errorMessage: msg}; if(err.name) errRtn.name = err.name return errRtn; } } function getSessionId() { var rtn = null try { const session_text = sessionStorage.getItem(DFIDO2_LIB_LOCALSTG_NAME_USER_SESSION) if(session_text){ const session_data = JSON.parse(session_text) rtn = session_data.session } return rtn } catch (err) { console.log(err) return null; } } async function validSession(rpId = null) { try { const session_text = sessionStorage.getItem(DFIDO2_LIB_LOCALSTG_NAME_USER_SESSION) if(!session_text) return false const session_data = JSON.parse(session_text) let req = {session: session_data.session} if (rpId && 0 < rpId.length) { req.rp = { id: rpId }; } const response = await fetch(localStorage.getItem(DFIDO2_LIB_LOCALSTG_NAME_SVR_URL) + "/usr/validsession", { method: "POST", cache: "no-cache", headers: { "Content-Type": "application/json" }, body: JSON.stringify(req) }); const resp = await response.json(); return resp.status === 'ok' } catch (err) { console.log(err) return false; } } async function logoutFido2UserSession(){ const session_text = sessionStorage.getItem(DFIDO2_LIB_LOCALSTG_NAME_USER_SESSION) if(!session_text) return const session_data = JSON.parse(session_text) let req = {session: session_data['session'], username: session_data['uid']} const response = await fetch(localStorage.getItem(DFIDO2_LIB_LOCALSTG_NAME_SVR_URL) + "/usr/delsession", { method: "POST", cache: "no-cache", headers: { "Content-Type": "application/json" }, body: JSON.stringify(req) }); sessionStorage.removeItem(DFIDO2_LIB_LOCALSTG_NAME_USER_SESSION); } async function getRegistrationUser(reg_session_id){ try { let req = {session_id: reg_session_id} const response = await fetch(localStorage.getItem(DFIDO2_LIB_LOCALSTG_NAME_SVR_URL) + "/reg/username", { method: "POST", cache: "no-cache", headers: { "Content-Type": "application/json" }, body: JSON.stringify(req) }); const resp = await response.json(); return resp.username } catch (err) { console.log(err) return false; } } function errProcessFido2(result){ alert(errMessageFido2(result)); } function errMessageFido2(result){ var rtn if(result.errCode && fido2LibErrCodes.unknown != result.errCode ){ switch (result.errCode){ case fido2LibErrCodes.user_canceled: rtn=getI18NErrorMessage('Fido2LibErr102:'); break; case fido2LibErrCodes.timeout: rtn=getI18NErrorMessage('Fido2LibErr103:'); break; default: rtn=result.errorMessage?result.errorMessage:getI18NErrorMessage('Fido2LibErr104:'); } }else if(result.name && "InvalidStateError" === result.name){ rtn=getI18NErrorMessage('Fido2LibErr105:'); }else if(result.errorMessage){ const msg = getI18NErrorMessage(result.errorMessage); rtn=msg?msg:result.errorMessage; }else{ rtn=getI18NErrorMessage(i18n_messages, 'Fido2LibErr104:'); } return rtn; } const fido2LibErrCodes = { user_canceled : -101, timeout : -102, unknown : -999 } const errMsgs = new Map(); const fido2LibErrMsgLanguages = { english: 'en-US', japanese: 'ja', chinese_cn: 'zh-CN', //chinese_tw: 'zh-TW', } errMsgs.set(fido2LibErrMsgLanguages.english, new Map()); errMsgs.get(fido2LibErrMsgLanguages.english).set('SvrErr101:', 'Unregistered enterprise authenticator aaguid!'); errMsgs.get(fido2LibErrMsgLanguages.english).set('SvrErr102:', 'Unable to authenticate with a unique device binding key from another device!'); errMsgs.get(fido2LibErrMsgLanguages.english).set('SvrErr103:', 'Unable to verify signature!'); errMsgs.get(fido2LibErrMsgLanguages.english).set('SvrErr104:', 'Key not found!'); errMsgs.get(fido2LibErrMsgLanguages.english).set('SvrErr105:', 'Username does not exist!'); errMsgs.get(fido2LibErrMsgLanguages.english).set('SvrErr106:', 'Unique Device ID is null!'); errMsgs.get(fido2LibErrMsgLanguages.english).set('SvrErr107:', '/attestation/result request body has no ID field!'); errMsgs.get(fido2LibErrMsgLanguages.english).set('SvrErr108:', 'ID field is not Base64Url encoded in /attestation/result request body!'); errMsgs.get(fido2LibErrMsgLanguages.english).set('SvrErr109:', '/attestation/result request body has no TYPE field!'); errMsgs.get(fido2LibErrMsgLanguages.english).set('SvrErr110:', 'TYPE field is not a DOMString in /attestation/result request body!'); errMsgs.get(fido2LibErrMsgLanguages.english).set('SvrErr111:', 'The TYPE field is not a public key in the /attestation/result request body!'); errMsgs.get(fido2LibErrMsgLanguages.english).set('SvrErr112:', 'ID field is not a DOMString in /attestation/result request body!'); errMsgs.get(fido2LibErrMsgLanguages.english).set('SvrErr115:', 'authenticatorData not found!'); errMsgs.get(fido2LibErrMsgLanguages.english).set('SvrErr116:', 'authenticatorData is not base64 URL encoded!'); errMsgs.get(fido2LibErrMsgLanguages.english).set('SvrErr117:', 'Signature not found!'); errMsgs.get(fido2LibErrMsgLanguages.english).set('SvrErr118:', 'Signature is not base64 URL encoded!'); errMsgs.get(fido2LibErrMsgLanguages.english).set('SvrErr119:', 'No user session!'); errMsgs.get(fido2LibErrMsgLanguages.english).set('SvrErr120:', 'User has reached the device limit!'); errMsgs.get(fido2LibErrMsgLanguages.english).set('Fido2LibErr101:', 'Your browser does not support FIDO2.'); errMsgs.get(fido2LibErrMsgLanguages.english).set('Fido2LibErr102:', 'The user canceled.'); errMsgs.get(fido2LibErrMsgLanguages.english).set('Fido2LibErr103:', 'The process timeout.'); errMsgs.get(fido2LibErrMsgLanguages.english).set('Fido2LibErr104:', 'System error.'); errMsgs.get(fido2LibErrMsgLanguages.english).set('Fido2LibErr105:', 'The same authenticator cannot be registered again.'); errMsgs.set(fido2LibErrMsgLanguages.japanese, new Map()); errMsgs.get(fido2LibErrMsgLanguages.japanese).set('SvrErr101:', '登録されていないエンタープライズ認証デバイス aaguid!'); errMsgs.get(fido2LibErrMsgLanguages.japanese).set('SvrErr102:', '別のデバイスからの一意のデバイス バインド キーで認証できません!'); errMsgs.get(fido2LibErrMsgLanguages.japanese).set('SvrErr103:', '署名を認証できません!'); errMsgs.get(fido2LibErrMsgLanguages.japanese).set('SvrErr104:', 'キーが見つかりません!'); errMsgs.get(fido2LibErrMsgLanguages.japanese).set('SvrErr105:', 'ユーザー名は存在しません!'); errMsgs.get(fido2LibErrMsgLanguages.japanese).set('SvrErr106:', '固有のデバイス ID が null です!'); errMsgs.get(fido2LibErrMsgLanguages.japanese).set('SvrErr107:', '/attestation/result request の本文に ID フィールドがありません!'); errMsgs.get(fido2LibErrMsgLanguages.japanese).set('SvrErr108:', 'ID フィールドは、/attestation/result リクエストの本文でエンコードされた Base64Url ではありません!'); errMsgs.get(fido2LibErrMsgLanguages.japanese).set('SvrErr109:', '/attestation/result リクエストのボディに TYPE フィールドがありません!'); errMsgs.get(fido2LibErrMsgLanguages.japanese).set('SvrErr110:', 'TYPE フィールドは、/attestation/result リクエストの本文の DOMString ではありません!'); errMsgs.get(fido2LibErrMsgLanguages.japanese).set('SvrErr111:', 'TYPE フィールドは、/attestation/result リクエストの本文の公開鍵ではありません!'); errMsgs.get(fido2LibErrMsgLanguages.japanese).set('SvrErr112:', 'ID フィールドは、/attestation/result リクエストの本文の DOMString ではありません!'); errMsgs.get(fido2LibErrMsgLanguages.japanese).set('SvrErr115:', 'authenticatorData が見つかりません!'); errMsgs.get(fido2LibErrMsgLanguages.japanese).set('SvrErr116:', 'authenticatorData は base64 URL エンコードされていません!'); errMsgs.get(fido2LibErrMsgLanguages.japanese).set('SvrErr117:', '署名が見つかりません!'); errMsgs.get(fido2LibErrMsgLanguages.japanese).set('SvrErr118:', '署名は base64 URL エンコードされていません!'); errMsgs.get(fido2LibErrMsgLanguages.japanese).set('SvrErr119:', 'ユーザーセッションがありません!'); errMsgs.get(fido2LibErrMsgLanguages.japanese).set('SvrErr120:', 'ユーザーはデバイスの制限数に達しました!'); errMsgs.get(fido2LibErrMsgLanguages.japanese).set('Fido2LibErr101:', 'お使いのブラウザは FIDO2 をサポートしていません。'); errMsgs.get(fido2LibErrMsgLanguages.japanese).set('Fido2LibErr102:', 'ユーザーがキャンセルしました。'); errMsgs.get(fido2LibErrMsgLanguages.japanese).set('Fido2LibErr103:', 'プロセスがタイムアウトしました。'); errMsgs.get(fido2LibErrMsgLanguages.japanese).set('Fido2LibErr104:', 'システムエラー。'); errMsgs.get(fido2LibErrMsgLanguages.japanese).set('Fido2LibErr105:', '同じ認証デバイスを再登録することはできません。'); errMsgs.set(fido2LibErrMsgLanguages.chinese_cn, new Map()); errMsgs.get(fido2LibErrMsgLanguages.chinese_cn).set('SvrErr101:', '未注册的企业认证器 aaguid!'); errMsgs.get(fido2LibErrMsgLanguages.chinese_cn).set('SvrErr102:', '无法使用来自其他设备的唯一设备绑定密钥进行身份验证!'); errMsgs.get(fido2LibErrMsgLanguages.chinese_cn).set('SvrErr103:', '无法验证签名!'); errMsgs.get(fido2LibErrMsgLanguages.chinese_cn).set('SvrErr104:', '认证Key未找到!'); errMsgs.get(fido2LibErrMsgLanguages.chinese_cn).set('SvrErr105:', '用户名不存在!'); errMsgs.get(fido2LibErrMsgLanguages.chinese_cn).set('SvrErr106:', 'Unique Device ID 为 null!'); errMsgs.get(fido2LibErrMsgLanguages.chinese_cn).set('SvrErr107:', '/attestation/result请求体没有ID字段!'); errMsgs.get(fido2LibErrMsgLanguages.chinese_cn).set('SvrErr108:', 'ID字段不是/attestation/result请求体中编码的Base64Url!'); errMsgs.get(fido2LibErrMsgLanguages.chinese_cn).set('SvrErr109:', '/attestation/result请求体没有TYPE字段!'); errMsgs.get(fido2LibErrMsgLanguages.chinese_cn).set('SvrErr110:', '/attestation/result 请求正文中的 TYPE 字段不是 DOMString!'); errMsgs.get(fido2LibErrMsgLanguages.chinese_cn).set('SvrErr111:', 'TYPE字段不是/attestation/result请求体中的公钥!'); errMsgs.get(fido2LibErrMsgLanguages.chinese_cn).set('SvrErr112:', 'ID 字段不是 /attestation/result 请求体中的 DOMString!'); errMsgs.get(fido2LibErrMsgLanguages.chinese_cn).set('SvrErr115:', 'authenticatorData 未找到!'); errMsgs.get(fido2LibErrMsgLanguages.chinese_cn).set('SvrErr116:', 'authenticatorData 不是 base64 URL 编码!'); errMsgs.get(fido2LibErrMsgLanguages.chinese_cn).set('SvrErr117:', '未找到签名!'); errMsgs.get(fido2LibErrMsgLanguages.chinese_cn).set('SvrErr118:', '签名不是 base64 URL 编码!'); errMsgs.get(fido2LibErrMsgLanguages.chinese_cn).set('SvrErr119:', '未建立用户会话!'); errMsgs.get(fido2LibErrMsgLanguages.chinese_cn).set('SvrErr120:', '用户已达到设备限制数!'); errMsgs.get(fido2LibErrMsgLanguages.chinese_cn).set('Fido2LibErr101:', '您的浏览器不支持FIDO2.'); errMsgs.get(fido2LibErrMsgLanguages.chinese_cn).set('Fido2LibErr102:', '用户取消了操作。'); errMsgs.get(fido2LibErrMsgLanguages.chinese_cn).set('Fido2LibErr103:', '操作超时。'); errMsgs.get(fido2LibErrMsgLanguages.chinese_cn).set('Fido2LibErr104:', '系统错误。'); errMsgs.get(fido2LibErrMsgLanguages.chinese_cn).set('Fido2LibErr105:', '无法再次注册相同的认证器。'); /** * * @param {String} errorMessage * @param {errMsgLanguages} language */ function getI18NErrorMessage(errorMessage, language = null){ var lang = language ? language : window.navigator.language var msgs = errMsgs.get(lang) if(!msgs)msgs = errMsgs.get(fido2LibErrMsgLanguages.english) if(errorMessage){ const msgHeader = 0 process_time_limit){ errRtn.errCode = fido2LibErrCodes.timeout }else{ errRtn.errCode = fido2LibErrCodes.user_canceled } }else errRtn.errCode = fido2LibErrCodes.unknown return errRtn; } } async function doAssertion(username = null, rpId = null, userVerification = 'preferred') { var process_time_limit = Number.MAX_SAFE_INTEGER try { let authnOptions; /*if (!username) { authnOptions = { authenticatorSelection: { //authenticatorAttachment: "platform", userVerification: "discouraged" } }; } else { authnOptions = { username: username, authenticatorSelection: { //authenticatorAttachment: "platform", userVerification: "preferred" } }; }*/ authnOptions = { username: username, authenticatorSelection: { //authenticatorAttachment: "platform", userVerification: userVerification } }; if (rpId && 0 < rpId.length) { authnOptions.rp = { id: rpId }; } const response = await fetch(localStorage.getItem(DFIDO2_LIB_LOCALSTG_NAME_SVR_URL) + "/assertion/options", { method: "POST", cache: "no-cache", headers: { "Content-Type": "application/json" }, body: JSON.stringify(authnOptions) }); const resp = await response.json(); if ('ok' === resp.status) { process_time_limit = (new Date()).getTime() + resp.timeout; resp.allowCredentials = resp.allowCredentials || []; let mappedAllowCreds = resp.allowCredentials.map(x => { return { id: _base64ToArrayBuffer(_fromBase64URL(x.id)), type: x.type, transports: x.transports // can set like ['internal', 'usb'] to override server settings }; }); const cred = await navigator.credentials.get({ publicKey: { challenge: _base64ToArrayBuffer(_fromBase64URL(resp.challenge)), timeout: resp.timeout, rpId: resp.rpId, userVerification: resp.userVerification, allowCredentials: mappedAllowCreds } }); if (cred) { let authRequest = { id: cred.id, rawId: Array.from(new Uint8Array(cred.rawId)), type: cred.type, response: { authenticatorData: _toBase64URL(btoa(_bufferToString(cred.response.authenticatorData))), clientDataJSON: _toBase64URL(btoa(_bufferToString(cred.response.clientDataJSON))), signature: _toBase64URL(btoa(_bufferToString(cred.response.signature))), userHandle: _toBase64URL(btoa(_bufferToString(cred.response.userHandle))) //_toBase64URL(btoa(_bufferToString(cred.response.userHandle))) } }; const res = await fetch(localStorage.getItem(DFIDO2_LIB_LOCALSTG_NAME_SVR_URL) + "/assertion/result", { method: "POST", cache: "no-cache", headers: { "Content-Type": "application/json" }, body: JSON.stringify(authRequest) }); const result = await res.json(); if (result.status === 'ok') { return result } else { return {status:'failed', errorMessage: result.errorMessage} } } else { return {status:'failed', errorMessage: 'Fido2LibErr999:Undefined Result'}; } } else { return {status:'failed', errorMessage: resp.errorMessage} } } catch (err) { var errRtn = {status:'failed', errorMessage: err.message}; if(err.name) errRtn.name = err.name if(err.name && 'NotAllowedError' === err.name){ const nowtm = (new Date()).getTime() if(nowtm > process_time_limit){ errRtn.errCode = fido2LibErrCodes.timeout }else{ errRtn.errCode = fido2LibErrCodes.user_canceled } }else errRtn.errCode = fido2LibErrCodes.unknown return errRtn; } } function _toBase64URL(s) { return (s = (s = (s = s.split("=")[0]).replace(/\+/g, "-")).replace(/\//g, "_")); } function _base64ToArrayBuffer(base64) { var binary_string = window.atob(base64); var len = binary_string.length; var bytes = new Uint8Array(len); for (var i = 0; i < len; i++) { bytes[i] = binary_string.charCodeAt(i); } return bytes; } function _stringToArrayBuffer(src) { return (new Uint8Array([].map.call(src, function (c) { return c.charCodeAt(0) }))).buffer; } function _fromBase64URL(s) { var chk = (s = s.replace(/-/g, "+").replace(/_/g, "/")).length % 4; if (chk) { if (1 === chk) throw new Error("Base64url string is wrong."); s += new Array(5 - chk).join("="); } return s; } function _bufferToString(s) { return new Uint8Array(s).reduce((s, e) => s + String.fromCodePoint(e), ""); }