diff options
| author | Mary <Ipadlover8322@gmail.com> | 2019-07-03 07:18:07 -0400 |
|---|---|---|
| committer | syuilo <Syuilotan@yahoo.co.jp> | 2019-07-03 20:18:07 +0900 |
| commit | fd94b817abd8fa628586746eed3a1f61b4a2b3d8 (patch) | |
| tree | 53eccf1b923f9b29f73ec6651b361b1682af3247 /src/client/app/common/views/components/settings | |
| parent | Resolve #5072 (diff) | |
| download | misskey-fd94b817abd8fa628586746eed3a1f61b4a2b3d8.tar.gz misskey-fd94b817abd8fa628586746eed3a1f61b4a2b3d8.tar.bz2 misskey-fd94b817abd8fa628586746eed3a1f61b4a2b3d8.zip | |
Implement Webauthn ๐ (#5088)
* Implement Webauthn :tada:
* Share hexifyAB
* Move hr inside template and add AttestationChallenges janitor daemon
* Apply suggestions from code review
Co-Authored-By: Acid Chicken (็กซ้
ธ้ถ) <root@acid-chicken.com>
* Add newline at the end of file
* Fix stray newline in promise chain
* Ignore var in try{}catch(){} block
Co-Authored-By: Acid Chicken (็กซ้
ธ้ถ) <root@acid-chicken.com>
* Add missing comma
* Add missing semicolon
* Support more attestation formats
* add support for more key types and linter pass
* Refactor
* Refactor
* credentialId --> id
* Fix
* Improve readability
* Add indexes
* fixes for credentialId->id
* Avoid changing store state
* Fix syntax error and code style
* Remove unused import
* Refactor of getkey API
* Create 1561706992953-webauthn.ts
* Update ja-JP.yml
* Add type annotations
* Fix code style
* Specify depedency version
* Fix code style
* Fix janitor daemon and login requesting 2FA regardless of status
Diffstat (limited to 'src/client/app/common/views/components/settings')
| -rw-r--r-- | src/client/app/common/views/components/settings/2fa.vue | 163 |
1 files changed, 162 insertions, 1 deletions
diff --git a/src/client/app/common/views/components/settings/2fa.vue b/src/client/app/common/views/components/settings/2fa.vue index 6e8d19d83a..eb645898e2 100644 --- a/src/client/app/common/views/components/settings/2fa.vue +++ b/src/client/app/common/views/components/settings/2fa.vue @@ -1,11 +1,54 @@ <template> -<div class="2fa"> +<div class="2fa totp-section"> <p style="margin-top:0;">{{ $t('intro') }}<a :href="$t('url')" target="_blank">{{ $t('detail') }}</a></p> <ui-info warn>{{ $t('caution') }}</ui-info> <p v-if="!data && !$store.state.i.twoFactorEnabled"><ui-button @click="register">{{ $t('register') }}</ui-button></p> <template v-if="$store.state.i.twoFactorEnabled"> + <h2 class="heading">{{ $t('totp-header') }}</h2> <p>{{ $t('already-registered') }}</p> <ui-button @click="unregister">{{ $t('unregister') }}</ui-button> + + <template v-if="supportsCredentials"> + <hr class="totp-method-sep"> + + <h2 class="heading">{{ $t('security-key-header') }}</h2> + <p>{{ $t('security-key') }}</p> + <div class="key-list"> + <div class="key" v-for="key in $store.state.i.securityKeysList"> + <h3> + {{ key.name }} + </h3> + <div class="last-used"> + {{ $t('last-used') }} + <mk-time :time="key.lastUsed"/> + </div> + <ui-button @click="unregisterKey(key)"> + {{ $t('unregister') }} + </ui-button> + </div> + </div> + + <ui-info warn v-if="registration && registration.error">{{ $t('something-went-wrong') }} {{ registration.error }}</ui-info> + <ui-button v-if="!registration || registration.error" @click="addSecurityKey">{{ $t('register') }}</ui-button> + + <ol v-if="registration && !registration.error"> + <li v-if="registration.stage >= 0"> + {{ $t('activate-key') }} + <fa icon="spinner" pulse fixed-width v-if="registration.saving && registration.stage == 0" /> + </li> + <li v-if="registration.stage >= 1"> + <ui-form :disabled="registration.stage != 1 || registration.saving"> + <ui-input v-model="keyName" :max="30"> + <span>{{ $t('security-key-name') }}</span> + </ui-input> + <ui-button @click="registerKey" :disabled="this.keyName.length == 0"> + {{ $t('register-security-key') }} + </ui-button> + <fa icon="spinner" pulse fixed-width v-if="registration.saving && registration.stage == 1" /> + </ui-form> + </li> + </ol> + </template> </template> <div v-if="data && !$store.state.i.twoFactorEnabled"> <ol> @@ -24,12 +67,21 @@ <script lang="ts"> import Vue from 'vue'; import i18n from '../../../../i18n'; +import { hostname } from '../../../../config'; +import { hexifyAB } from '../../../scripts/2fa'; + +function stringifyAB(buffer) { + return String.fromCharCode.apply(null, new Uint8Array(buffer)); +} export default Vue.extend({ i18n: i18n('desktop/views/components/settings.2fa.vue'), data() { return { data: null, + supportsCredentials: !!navigator.credentials, + registration: null, + keyName: '', token: null }; }, @@ -76,7 +128,116 @@ export default Vue.extend({ }).catch(() => { this.$notify(this.$t('failed')); }); + }, + + registerKey() { + this.registration.saving = true; + this.$root.api('i/2fa/key-done', { + password: this.registration.password, + 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) + }).then(key => { + this.registration = null; + key.lastUsed = new Date(); + this.$notify(this.$t('success')); + }) + }, + + unregisterKey(key) { + this.$root.dialog({ + title: this.$t('enter-password'), + input: { + type: 'password' + } + }).then(({ canceled, result: password }) => { + if (canceled) return; + return this.$root.api('i/2fa/remove-key', { + password, + credentialId: key.id + }).then(() => { + this.$notify(this.$t('key-unregistered')); + }); + }); + }, + + addSecurityKey() { + this.$root.dialog({ + title: this.$t('enter-password'), + input: { + type: 'password' + } + }).then(({ canceled, result: password }) => { + if (canceled) return; + this.$root.api('i/2fa/register-key', { + password + }).then(registration => { + this.registration = { + password, + challengeId: registration.challengeId, + stage: 0, + publicKeyOptions: { + challenge: Buffer.from( + registration.challenge + .replace(/\-/g, "+") + .replace(/_/g, "/"), + 'base64' + ), + rp: { + id: hostname, + name: 'Misskey' + }, + user: { + id: Uint8Array.from(this.$store.state.i.id, c => c.charCodeAt(0)), + name: this.$store.state.i.username, + displayName: this.$store.state.i.name, + }, + pubKeyCredParams: [{alg: -7, type: 'public-key'}], + timeout: 60000, + attestation: 'direct' + }, + saving: true + }; + return navigator.credentials.create({ + publicKey: this.registration.publicKeyOptions + }); + }).then(credential => { + this.registration.credential = credential; + this.registration.saving = false; + this.registration.stage = 1; + }).catch(err => { + console.warn('Error while registering?', err); + this.registration.error = err.message; + this.registration.stage = -1; + }); + }); } } }); </script> + +<style lang="stylus" scoped> +.totp-section + .totp-method-sep + margin 1.5em 0 1em + border none + border-top solid var(--lineWidth) var(--faceDivider) + + h2.heading + margin 0 + + .key + padding 1em + margin 0.5em 0 + background #161616 + border-radius 6px + + h3 + margin-top 0 + margin-bottom .3em + + .last-used + margin-bottom .5em +</style> |