From c348688eebdd7f5f28760e138a7ac152d2099d9e Mon Sep 17 00:00:00 2001 From: Satsuki Yanagi <17376330+u1-liquid@users.noreply.github.com> Date: Sat, 9 May 2020 08:20:22 +0900 Subject: [PATCH] =?UTF-8?q?WebAuthn=E3=81=A7=E3=83=AD=E3=82=B0=E3=82=A4?= =?UTF-8?q?=E3=83=B3=E3=81=A7=E3=81=8D=E3=81=AA=E3=81=84=E3=81=AE=E3=82=92?= =?UTF-8?q?=E4=BF=AE=E6=AD=A3=20(#6327)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Resolve #6319 --- locales/ja-JP.yml | 2 +- src/client/components/signin.vue | 17 +++++-------- src/client/pages/my-settings/2fa.vue | 23 ++++++------------ src/client/scripts/2fa.ts | 36 ++++++++++++++++++++++++---- 4 files changed, 46 insertions(+), 32 deletions(-) diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index ce9b0dcd6..5bc978492 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -414,7 +414,7 @@ passwordMatched: "一致しました" passwordNotMatched: "一致していません" signinWith: "{x}でログイン" signinFailed: "ログインできませんでした。ユーザー名とパスワードを確認してください。" -tapSecurityKey: "セキュリティーキーにタッチ" +tapSecurityKey: "セキュリティキーにタッチ" or: "もしくは" uiLanguage: "UIの表示言語" groupInvited: "グループに招待されました" diff --git a/src/client/components/signin.vue b/src/client/components/signin.vue index b84b89a5c..dc73ad8a0 100755 --- a/src/client/components/signin.vue +++ b/src/client/components/signin.vue @@ -51,7 +51,7 @@ import MkButton from './ui/button.vue'; import MkInput from './ui/input.vue'; import i18n from '../i18n'; import { apiUrl, host } from '../config'; -import { hexifyAB } from '../scripts/2fa'; +import { byteify, hexify } from '../scripts/2fa'; export default Vue.extend({ i18n, @@ -121,14 +121,9 @@ export default Vue.extend({ this.queryingKey = true; return navigator.credentials.get({ publicKey: { - challenge: Buffer.from( - this.challengeData.challenge - .replace(/\-/g, '+') - .replace(/_/g, '/'), - 'base64' - ), + challenge: byteify(this.challengeData.challenge, 'base64'), allowCredentials: this.challengeData.securityKeys.map(key => ({ - id: Buffer.from(key.id, 'hex'), + id: byteify(key.id, 'hex'), type: 'public-key', transports: ['usb', 'nfc', 'ble', 'internal'] })), @@ -143,9 +138,9 @@ export default Vue.extend({ return this.$root.api('signin', { username: this.username, password: this.password, - signature: hexifyAB(credential.response.signature), - authenticatorData: hexifyAB(credential.response.authenticatorData), - clientDataJSON: hexifyAB(credential.response.clientDataJSON), + signature: hexify(credential.response.signature), + authenticatorData: hexify(credential.response.authenticatorData), + clientDataJSON: hexify(credential.response.clientDataJSON), credentialId: credential.id, challengeId: this.challengeData.challengeId }); diff --git a/src/client/pages/my-settings/2fa.vue b/src/client/pages/my-settings/2fa.vue index 1d1e111da..6ceca21fe 100644 --- a/src/client/pages/my-settings/2fa.vue +++ b/src/client/pages/my-settings/2fa.vue @@ -22,12 +22,12 @@ {{ $t('passwordLessLogin') }} - {{ $t('something-went-wrong') }} {{ registration.error }} + {{ $t('error') }} {{ registration.error }} {{ $t('_2fa.registerKey') }}
  1. - {{ $t('activate-key') }} + {{ $t('tapSecurityKey') }}
  2. @@ -67,16 +67,12 @@ import Vue from 'vue'; import { faLock } from '@fortawesome/free-solid-svg-icons'; import i18n from '../../i18n'; import { hostname } from '../../config'; -import { hexifyAB } from '../../scripts/2fa'; +import { byteify, hexify, stringify } from '../../scripts/2fa'; import MkButton from '../../components/ui/button.vue'; import MkInfo from '../../components/ui/info.vue'; import MkInput from '../../components/ui/input.vue'; import MkSwitch from '../../components/ui/switch.vue'; -function stringifyAB(buffer) { - return String.fromCharCode.apply(null, new Uint8Array(buffer)); -} - export default Vue.extend({ i18n, components: { @@ -157,8 +153,8 @@ export default Vue.extend({ name: this.keyName, challengeId: this.registration.challengeId, // we convert each 16 bits to a string to serialise - clientDataJSON: stringifyAB(this.registration.credential.response.clientDataJSON), - attestationObject: hexifyAB(this.registration.credential.response.attestationObject) + clientDataJSON: stringify(this.registration.credential.response.clientDataJSON), + attestationObject: hexify(this.registration.credential.response.attestationObject) }).then(key => { this.registration = null; key.lastUsed = new Date(); @@ -208,18 +204,13 @@ export default Vue.extend({ challengeId: registration.challengeId, stage: 0, publicKeyOptions: { - challenge: Uint8Array.from( - atob(registration.challenge - .replace(/\-/g, '+') - .replace(/_/g, '/')), - x => x.charCodeAt(0), - ), + challenge: byteify(registration.challenge, 'base64'), rp: { id: hostname, name: 'Misskey' }, user: { - id: Uint8Array.from(this.$store.state.i.id, c => c.charCodeAt(0)), + id: byteify(this.$store.state.i.id, 'ascii'), name: this.$store.state.i.username, displayName: this.$store.state.i.name, }, diff --git a/src/client/scripts/2fa.ts b/src/client/scripts/2fa.ts index e431361aa..00363cffa 100644 --- a/src/client/scripts/2fa.ts +++ b/src/client/scripts/2fa.ts @@ -1,5 +1,33 @@ -export function hexifyAB(buffer) { - return Array.from(new Uint8Array(buffer)) - .map(item => item.toString(16).padStart(2, '0')) - .join(''); +export function byteify(data: string, encoding: 'ascii' | 'base64' | 'hex') { + switch (encoding) { + case 'ascii': + return Uint8Array.from(data, c => c.charCodeAt(0)); + case 'base64': + return Uint8Array.from( + atob( + data + .replace(/-/g, '+') + .replace(/_/g, '/') + ), + c => c.charCodeAt(0) + ); + case 'hex': + return new Uint8Array( + data + .match(/.{1,2}/g) + .map(byte => parseInt(byte, 16)) + ); + } +} + +export function hexify(buffer: ArrayBuffer) { + return Array.from(new Uint8Array(buffer)) + .reduce( + (str, byte) => str + byte.toString(16).padStart(2, '0'), + '' + ); +} + +export function stringify(buffer: ArrayBuffer) { + return String.fromCharCode(... new Uint8Array(buffer)); }