From 9c76e03c3aad2a1dbe9e0f348c67162b34fe88be Mon Sep 17 00:00:00 2001 From: Mary Date: Wed, 3 Jul 2019 07:18:07 -0400 Subject: [PATCH] =?UTF-8?q?Implement=20Webauthn=20=F0=9F=8E=89=20(#5088)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Implement Webauthn :tada: * Share hexifyAB * Move hr inside template and add AttestationChallenges janitor daemon * Apply suggestions from code review Co-Authored-By: Acid Chicken (硫酸鶏) * Add newline at the end of file * Fix stray newline in promise chain * Ignore var in try{}catch(){} block Co-Authored-By: Acid Chicken (硫酸鶏) * 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 --- locales/en-US.yml | 13 +- locales/ja-JP.yml | 11 + migration/1561706992953-webauthn.ts | 29 ++ package.json | 4 + src/boot/master.ts | 1 + src/client/app/common/scripts/2fa.ts | 5 + .../common/views/components/settings/2fa.vue | 163 ++++++- .../app/common/views/components/signin.vue | 182 ++++++-- src/daemons/janitor.ts | 18 + src/db/postgre.ts | 6 +- src/models/entities/attestation-challenge.ts | 46 ++ src/models/entities/user-profile.ts | 5 + src/models/entities/user-security-key.ts | 48 ++ src/models/index.ts | 4 + src/models/repositories/user.ts | 16 +- src/server/api/2fa.ts | 422 ++++++++++++++++++ src/server/api/endpoints/i/2fa/getkeys.ts | 67 +++ src/server/api/endpoints/i/2fa/key-done.ts | 151 +++++++ .../api/endpoints/i/2fa/register-key.ts | 60 +++ src/server/api/endpoints/i/2fa/remove-key.ts | 46 ++ src/server/api/private/signin.ts | 143 ++++-- 21 files changed, 1376 insertions(+), 64 deletions(-) create mode 100644 migration/1561706992953-webauthn.ts create mode 100644 src/client/app/common/scripts/2fa.ts create mode 100644 src/daemons/janitor.ts create mode 100644 src/models/entities/attestation-challenge.ts create mode 100644 src/models/entities/user-security-key.ts create mode 100644 src/server/api/2fa.ts create mode 100644 src/server/api/endpoints/i/2fa/getkeys.ts create mode 100644 src/server/api/endpoints/i/2fa/key-done.ts create mode 100644 src/server/api/endpoints/i/2fa/register-key.ts create mode 100644 src/server/api/endpoints/i/2fa/remove-key.ts diff --git a/locales/en-US.yml b/locales/en-US.yml index fd8e96588..158b8c649 100644 --- a/locales/en-US.yml +++ b/locales/en-US.yml @@ -601,6 +601,8 @@ common/views/components/signin.vue: signin-with-github: "Sign in with GitHub" signin-with-discord: "Sign in with Discord" login-failed: "Logging in has failed. Make sure you have entered the correct username and password." + tap-key: "Activate your security key by tapping or clicking it to login" + enter-2fa-code: "Enter your 2FA code below" common/views/components/signup.vue: invitation-code: "Invitation code" invitation-info: "If you do not have an invitation code, please contact an administrator." @@ -984,7 +986,7 @@ desktop/views/components/settings.2fa.vue: url: "https://www.google.com/landing/2step/" caution: "If you lose access to your registered device, you won't be able to connect to Misskey anymore!" register: "Register a device" - already-registered: "This device is already registered" + already-registered: "Your account is currently registered to an authenticator application" unregister: "Unregister" unregistered: "Two-factor authentication has been disabled." enter-password: "Enter the password" @@ -997,6 +999,15 @@ desktop/views/components/settings.2fa.vue: success: "Settings saved!" failed: "Failed to setup. Please ensure that the token is correct." info: "From the next time you sign in to Misskey, the token displayed on your device will be necessary too, as well as the password." + totp-header: "Authenticator App" + security-key-header: "Security Keys" + security-key: "You can use a hardware security key supporting FIDO2 to log into your account for enhanced security. When you sign-in, you'll need a registered security key or your authenticator app." + last-used: "Last used:" + activate-key: "Please activate your security key by tapping or clicking it" + security-key-name: "Key Name" + register-security-key: "Finish Key Registration" + something-went-wrong: "Oops! Something went wrong while trying to register your key:" + key-unregistered: "Key Removed" common/views/components/media-image.vue: sensitive: "NSFW" click-to-show: "Click to show" diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 9d104456f..5767a51b0 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -646,6 +646,8 @@ common/views/components/signin.vue: signin-with-github: "GitHubでログイン" signin-with-discord: "Discordでログイン" login-failed: "ログインできませんでした。ユーザー名とパスワードを確認してください。" + tap-key: "セキュリティキーをクリックしてログイン" + enter-2fa-code: "認証コードを入力してください" common/views/components/signup.vue: invitation-code: "招待コード" @@ -1100,6 +1102,15 @@ desktop/views/components/settings.2fa.vue: success: "設定が完了しました!" failed: "設定に失敗しました。トークンに誤りがないかご確認ください。" info: "次回サインインからは、同様にパスワードに加えてデバイスに表示されているトークンを入力します。" + totp-header: "認証アプリ" + security-key-header: "セキュリティキー" + security-key: "セキュリティを強化するために、FIDO2をサポートするハードウェアセキュリティキーを使用してアカウントにログインできます。 サインインの際は、登録されたセキュリティキーまたは認証アプリが必要になります。" + last-used: "最後の使用:" + activate-key: "クリックしてセキュリティキーをアクティベートしてください" + security-key-name: "キー名" + register-security-key: "キーの登録を完了" + something-went-wrong: "わー! キーを登録する際に問題が発生しました:" + key-unregistered: "キーが削除されました" common/views/components/media-image.vue: sensitive: "閲覧注意" diff --git a/migration/1561706992953-webauthn.ts b/migration/1561706992953-webauthn.ts new file mode 100644 index 000000000..fc1f0c042 --- /dev/null +++ b/migration/1561706992953-webauthn.ts @@ -0,0 +1,29 @@ +import {MigrationInterface, QueryRunner} from "typeorm"; + +export class webauthn1561706992953 implements MigrationInterface { + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`CREATE TABLE "attestation_challenge" ("id" character varying(32) NOT NULL, "userId" character varying(32) NOT NULL, "challenge" character varying(64) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "registrationChallenge" boolean NOT NULL DEFAULT false, CONSTRAINT "PK_d0ba6786e093f1bcb497572a6b5" PRIMARY KEY ("id", "userId"))`); + await queryRunner.query(`CREATE INDEX "IDX_f1a461a618fa1755692d0e0d59" ON "attestation_challenge" ("userId") `); + await queryRunner.query(`CREATE INDEX "IDX_47efb914aed1f72dd39a306c7b" ON "attestation_challenge" ("challenge") `); + await queryRunner.query(`CREATE TABLE "user_security_key" ("id" character varying NOT NULL, "userId" character varying(32) NOT NULL, "publicKey" character varying NOT NULL, "lastUsed" TIMESTAMP WITH TIME ZONE NOT NULL, "name" character varying(30) NOT NULL, CONSTRAINT "PK_3e508571121ab39c5f85d10c166" PRIMARY KEY ("id"))`); + await queryRunner.query(`CREATE INDEX "IDX_ff9ca3b5f3ee3d0681367a9b44" ON "user_security_key" ("userId") `); + await queryRunner.query(`CREATE INDEX "IDX_0d7718e562dcedd0aa5cf2c9f7" ON "user_security_key" ("publicKey") `); + await queryRunner.query(`ALTER TABLE "user_profile" ADD "securityKeysAvailable" boolean NOT NULL DEFAULT false`); + await queryRunner.query(`ALTER TABLE "attestation_challenge" ADD CONSTRAINT "FK_f1a461a618fa1755692d0e0d592" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "user_security_key" ADD CONSTRAINT "FK_ff9ca3b5f3ee3d0681367a9b447" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "user_security_key" DROP CONSTRAINT "FK_ff9ca3b5f3ee3d0681367a9b447"`); + await queryRunner.query(`ALTER TABLE "attestation_challenge" DROP CONSTRAINT "FK_f1a461a618fa1755692d0e0d592"`); + await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "securityKeysAvailable"`); + await queryRunner.query(`DROP INDEX "IDX_0d7718e562dcedd0aa5cf2c9f7"`); + await queryRunner.query(`DROP INDEX "IDX_ff9ca3b5f3ee3d0681367a9b44"`); + await queryRunner.query(`DROP TABLE "user_security_key"`); + await queryRunner.query(`DROP INDEX "IDX_47efb914aed1f72dd39a306c7b"`); + await queryRunner.query(`DROP INDEX "IDX_f1a461a618fa1755692d0e0d59"`); + await queryRunner.query(`DROP TABLE "attestation_challenge"`); + } + +} diff --git a/package.json b/package.json index 79009380c..119deacaf 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,7 @@ "@koa/cors": "3.0.0", "@types/bcryptjs": "2.4.2", "@types/bull": "3.5.15", + "@types/cbor": "2.0.0", "@types/dateformat": "3.0.0", "@types/deep-equal": "1.0.1", "@types/double-ended-queue": "2.1.1", @@ -104,9 +105,11 @@ "autosize": "4.0.2", "autwh": "0.1.0", "bcryptjs": "2.4.3", + "bootstrap": "4.3.1", "bootstrap-vue": "2.0.0-rc.13", "bull": "3.10.0", "cafy": "15.1.1", + "cbor": "4.1.5", "chai": "4.2.0", "chalk": "2.4.2", "cli-highlight": "2.1.1", @@ -148,6 +151,7 @@ "jsdom": "15.1.1", "json5": "2.1.0", "json5-loader": "3.0.0", + "jsrsasign": "8.0.12", "katex": "0.10.2", "koa": "2.7.0", "koa-bodyparser": "4.2.1", diff --git a/src/boot/master.ts b/src/boot/master.ts index 6c23a528f..b698548d4 100644 --- a/src/boot/master.ts +++ b/src/boot/master.ts @@ -79,6 +79,7 @@ export async function masterMain() { require('../daemons/server-stats').default(); require('../daemons/notes-stats').default(); require('../daemons/queue-stats').default(); + require('../daemons/janitor').default(); } bootLogger.succ(`Now listening on port ${config.port} on ${config.url}`, null, true); diff --git a/src/client/app/common/scripts/2fa.ts b/src/client/app/common/scripts/2fa.ts new file mode 100644 index 000000000..f638cce15 --- /dev/null +++ b/src/client/app/common/scripts/2fa.ts @@ -0,0 +1,5 @@ +export function hexifyAB(buffer) { + return Array.from(new Uint8Array(buffer)) + .map(item => item.toString(16).padStart(2, 0)) + .join(''); +} diff --git a/src/client/app/common/views/components/settings/2fa.vue b/src/client/app/common/views/components/settings/2fa.vue index 6e8d19d83..eb645898e 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 @@