diff --git a/app/Repositories/UserPasskeyRepository.php b/app/Repositories/UserPasskeyRepository.php index a91c7ff6..e9128b03 100644 --- a/app/Repositories/UserPasskeyRepository.php +++ b/app/Repositories/UserPasskeyRepository.php @@ -15,7 +15,25 @@ class UserPasskeyRepository extends BaseRepository public static function createWebAuthn() { $formats = ['android-key', 'android-safetynet', 'apple', 'fido-u2f', 'packed', 'tpm', 'none']; - return new WebAuthn(get_setting('basic.SITENAME'), nexus()->getRequestHost(), $formats); + $rpId = explode(':', nexus()->getRequestHost())[0]; + return new WebAuthn(get_setting('basic.SITENAME'), $rpId, $formats); + } + + private static function putChallenge($challenge): string + { + $challengeId = bin2hex(random_bytes(32)); + NexusDB::cache_put("passkey_challenge_{$challengeId}", $challenge, 120); + return $challengeId; + } + + private static function getChallenge($challengeId) + { + $challenge = NexusDB::cache_get("passkey_challenge_{$challengeId}") ?? null; + if ($challenge == null) { + throw new RuntimeException(nexus_trans('passkey.passkey_timeout')); + } + NexusDB::cache_del("passkey_challenge_{$challengeId}"); + return $challenge; } public static function getCreateArgs($userId, $userName) @@ -28,20 +46,22 @@ class UserPasskeyRepository extends BaseRepository return hex2bin($item['credential_id']); }, $passkey->toArray()); - $createArgs = $WebAuthn->getCreateArgs(bin2hex($userId), $userName, $userName, 60 * 4, true, 'preferred', null, $credentialIds); + $createArgs = $WebAuthn->getCreateArgs(bin2hex($userId), $userName, $userName, 120, true, 'preferred', null, $credentialIds); + $challengeId = self::putChallenge($WebAuthn->getChallenge()); - NexusDB::cache_put("{$userId}_passkey_challenge", $WebAuthn->getChallenge(), 60 * 4); - - return $createArgs; + return [ + 'challengeId' => $challengeId, + 'options' => $createArgs, + ]; } - public static function processCreate($userId, $clientDataJSON, $attestationObject) + public static function processCreate($userId, $challengeId, $clientDataJSON, $attestationObject) { - $WebAuthn = self::createWebAuthn(); - + $challenge = self::getChallenge($challengeId); $clientDataJSON = !empty($clientDataJSON) ? base64_decode($clientDataJSON) : null; $attestationObject = !empty($attestationObject) ? base64_decode($attestationObject) : null; - $challenge = NexusDB::cache_get("{$userId}_passkey_challenge") ?? null; + + $WebAuthn = self::createWebAuthn(); $data = $WebAuthn->processCreate($clientDataJSON, $attestationObject, $challenge, false, true, false); @@ -60,9 +80,13 @@ class UserPasskeyRepository extends BaseRepository { $WebAuthn = self::createWebAuthn(); - $getArgs = $WebAuthn->getGetArgs(null, 60 * 4, true, true, true, true, true, 'preferred'); + $getArgs = $WebAuthn->getGetArgs(null, 120, true, true, true, true, true, 'preferred'); + $challengeId = self::putChallenge($WebAuthn->getChallenge()); - return $getArgs; + return [ + 'challengeId' => $challengeId, + 'options' => $getArgs, + ]; } public static function insertUserPasskey($userId, $AAGUID, $credentialId, $publicKey, $counter) @@ -77,21 +101,19 @@ class UserPasskeyRepository extends BaseRepository Passkey::query()->create($params); } - public static function processGet($challenge, $id, $clientDataJSON, $authenticatorData, $signature, $userHandle) + public static function processGet($challengeId, $id, $clientDataJSON, $authenticatorData, $signature, $userHandle) { - $WebAuthn = self::createWebAuthn(); - + $challenge = self::getChallenge($challengeId); $clientDataJSON = !empty($clientDataJSON) ? base64_decode($clientDataJSON) : null; $id = !empty($id) ? base64_decode($id) : null; $authenticatorData = !empty($authenticatorData) ? base64_decode($authenticatorData) : null; $signature = !empty($signature) ? base64_decode($signature) : null; $userHandle = !empty($userHandle) ? base64_decode($userHandle) : null; - $challenge = !empty($challenge) ? base64_decode($challenge) : null; + + $WebAuthn = self::createWebAuthn(); $passkey = Passkey::query()->where('credential_id', '=', bin2hex($id))->first(); - $credentialPublicKey = $passkey->public_key; - - if ($credentialPublicKey === null) { + if ($passkey === null) { throw new RuntimeException(nexus_trans('passkey.passkey_unknown')); } @@ -100,7 +122,7 @@ class UserPasskeyRepository extends BaseRepository } try { - $WebAuthn->processGet($clientDataJSON, $authenticatorData, $signature, $credentialPublicKey, $challenge, null, 'preferred'); + $WebAuthn->processGet($clientDataJSON, $authenticatorData, $signature, $passkey->public_key, $challenge, null, false, true); } catch (Exception $e) { throw new RuntimeException(nexus_trans('passkey.passkey_error') . "\n" . $e->getMessage()); } diff --git a/public/ajax.php b/public/ajax.php index 41a00499..f89b78b5 100644 --- a/public/ajax.php +++ b/public/ajax.php @@ -194,7 +194,7 @@ class AjaxInterface{ { global $CURUSER; $rep = new \App\Repositories\UserPasskeyRepository(); - return $rep->processCreate($CURUSER['id'], $params['clientDataJSON'], $params['attestationObject']); + return $rep->processCreate($CURUSER['id'], $params['challengeId'], $params['clientDataJSON'], $params['attestationObject']); } public static function deletePasskey($params) @@ -222,7 +222,7 @@ class AjaxInterface{ { global $CURUSER; $rep = new \App\Repositories\UserPasskeyRepository(); - return $rep->processGet($params['challenge'], $params['id'], $params['clientDataJSON'], $params['authenticatorData'], $params['signature'], $params['userHandle']); + return $rep->processGet($params['challengeId'], $params['id'], $params['clientDataJSON'], $params['authenticatorData'], $params['signature'], $params['userHandle']); } } diff --git a/public/js/passkey.js b/public/js/passkey.js index 9a87c342..08ac8a0e 100644 --- a/public/js/passkey.js +++ b/public/js/passkey.js @@ -13,9 +13,9 @@ const Passkey = (() => { return await PublicKeyCredential.isConditionalMediationAvailable(); } - const getCreateArgs = async () => { + const getArgs = async (type) => { const getArgsParams = new URLSearchParams(); - getArgsParams.set('action', 'getPasskeyCreateArgs'); + getArgsParams.set('action', type === 'create' ? 'getPasskeyCreateArgs' : 'getPasskeyGetArgs'); const response = await fetch(apiUrl, { method: 'POST', @@ -26,18 +26,22 @@ const Passkey = (() => { throw new Error(data.msg); } - const createArgs = data.data; - recursiveBase64StrToArrayBuffer(createArgs); - return createArgs; + const options = data.data.options; + recursiveBase64StrToArrayBuffer(options); + return { + challengeId: data.data.challengeId, + options: options, + }; } const createRegistration = async () => { - const createArgs = await getCreateArgs(); + const args = await getArgs('create'); - const cred = await navigator.credentials.create(createArgs); + const cred = await navigator.credentials.create(args.options); const processCreateParams = new URLSearchParams(); processCreateParams.set('action', 'processPasskeyCreate'); + processCreateParams.set('params[challengeId]', args.challengeId); processCreateParams.set('params[transports]', cred.response.getTransports ? cred.response.getTransports() : null) processCreateParams.set('params[clientDataJSON]', cred.response.clientDataJSON ? arrayBufferToBase64(cred.response.clientDataJSON) : null); processCreateParams.set('params[attestationObject]', cred.response.attestationObject ? arrayBufferToBase64(cred.response.attestationObject) : null); @@ -52,24 +56,6 @@ const Passkey = (() => { } } - const getGetArgs = async () => { - const getArgsParams = new URLSearchParams(); - getArgsParams.set('action', 'getPasskeyGetArgs'); - - const response = await fetch(apiUrl, { - method: 'POST', - body: getArgsParams, - }); - const data = await response.json(); - if (data.ret !== 0) { - throw new Error(data.msg); - } - - const getArgs = data.data; - recursiveBase64StrToArrayBuffer(getArgs); - return getArgs; - } - let abortController; const checkRegistration = async (conditional, showLoading) => { @@ -78,20 +64,21 @@ const Passkey = (() => { abortController = null; } if (!conditional) showLoading(); - const getArgs = await getGetArgs(); + const args = await getArgs('get'); + const options = args.options; if (conditional) { abortController = new AbortController(); - getArgs.signal = abortController.signal; - getArgs.mediation = 'conditional'; + options.signal = abortController.signal; + options.mediation = 'conditional'; } - const cred = await navigator.credentials.get(getArgs); + const cred = await navigator.credentials.get(options); if (conditional) showLoading(); const processGetParams = new URLSearchParams(); processGetParams.set('action', 'processPasskeyGet'); - processGetParams.set('params[challenge]', arrayBufferToBase64(getArgs['publicKey']['challenge'])); + processGetParams.set('params[challengeId]', args.challengeId); processGetParams.set('params[id]', cred.rawId ? arrayBufferToBase64(cred.rawId) : null); processGetParams.set('params[clientDataJSON]', cred.response.clientDataJSON ? arrayBufferToBase64(cred.response.clientDataJSON) : null); processGetParams.set('params[authenticatorData]', cred.response.authenticatorData ? arrayBufferToBase64(cred.response.authenticatorData) : null); diff --git a/resources/lang/en/passkey.php b/resources/lang/en/passkey.php index 5185da54..7b85cd45 100644 --- a/resources/lang/en/passkey.php +++ b/resources/lang/en/passkey.php @@ -11,6 +11,7 @@ return [ 'passkey_delete' => 'Delete', 'passkey_unknown' => 'An error occurred while processing your request.', 'passkey_invalid' => 'Invalid passkey data.', + 'passkey_timeout' => 'Operation timeout, please try again.', 'passkey_error' => 'An error occurred while processing your request. Please try again later.', 'passkey_user_not_found' => 'User not found.', 'passkey_not_supported' => 'Your browser does not support passkey. Please use a modern browser to create and manage your passkey.', diff --git a/resources/lang/zh_CN/passkey.php b/resources/lang/zh_CN/passkey.php index 3b699b76..f4199339 100644 --- a/resources/lang/zh_CN/passkey.php +++ b/resources/lang/zh_CN/passkey.php @@ -11,6 +11,7 @@ return [ 'passkey_delete' => '删除', 'passkey_unknown' => '处理您的请求时出错。', 'passkey_invalid' => '通行密钥数据无效。', + 'passkey_timeout' => '操作超时,请重试。', 'passkey_error' => '处理您的请求时出错。请稍后重试。', 'passkey_user_not_found' => '未找到用户。', 'passkey_not_supported' => '您的浏览器不支持通行密钥。请使用现代浏览器创建和管理您的通行密钥。',