diff --git a/locales/ar-SA.yml b/locales/ar-SA.yml index 3ce83ebcc8..7e97a99ebc 100644 --- a/locales/ar-SA.yml +++ b/locales/ar-SA.yml @@ -1049,8 +1049,8 @@ _tutorial: step6_4: "Now go, explore, and have fun!" _2fa: alreadyRegistered: "سجلت سلفًا جهازًا للاستيثاق بعاملين." - registerDevice: "سجّل جهازًا جديدًا" - registerKey: "تسجيل مفتاح أمان جديد" + registerTOTP: "سجّل جهازًا جديدًا" + registerSecurityKey: "تسجيل مفتاح أمان جديد" step1: "أولًا ثبّت تطبيق استيثاق على جهازك (مثل {a} و{b})." step2: "امسح رمز الاستجابة السريعة الموجد على الشاشة." step3: "أدخل الرمز الموجود في تطبيقك لإكمال التثبيت." diff --git a/locales/bn-BD.yml b/locales/bn-BD.yml index f08e4bfc2d..e3fbf8cb9b 100644 --- a/locales/bn-BD.yml +++ b/locales/bn-BD.yml @@ -1130,8 +1130,8 @@ _tutorial: step6_4: "Now go, explore, and have fun!" _2fa: alreadyRegistered: "আপনি ইতিমধ্যে একটি 2-ফ্যাক্টর অথেনটিকেশন ডিভাইস নিবন্ধন করেছেন৷" - registerDevice: "নতুন ডিভাইস নিবন্ধন করুন" - registerKey: "সিকিউরিটি কী নিবন্ধন করুন" + registerTOTP: "নতুন ডিভাইস নিবন্ধন করুন" + registerSecurityKey: "সিকিউরিটি কী নিবন্ধন করুন" step1: "প্রথমে, আপনার ডিভাইসে {a} বা {b} এর মতো একটি অথেনটিকেশন অ্যাপ ইনস্টল করুন৷" step2: "এরপরে, অ্যাপের সাহায্যে প্রদর্শিত QR কোডটি স্ক্যান করুন।" step2Url: "ডেস্কটপ অ্যাপে, নিম্নলিখিত URL লিখুন:" diff --git a/locales/ca-ES.yml b/locales/ca-ES.yml index b57fd1c0f5..20e67a0971 100644 --- a/locales/ca-ES.yml +++ b/locales/ca-ES.yml @@ -319,13 +319,13 @@ _sfx: _2fa: step2Url: "També pots inserir aquest enllaç i utilitzes una aplicació d'escriptori:" alreadyRegistered: Ja heu registrat un dispositiu d'autenticació de dos factors. - registerDevice: Registrar un dispositiu nou + registerTOTP: Registrar un dispositiu nou securityKeyInfo: A més de l'autenticació d'empremta digital o PIN, també podeu configurar l'autenticació mitjançant claus de seguretat de maquinari compatibles amb FIDO2 per protegir encara més el vostre compte. step4: A partir d'ara, qualsevol intent d'inici de sessió futur demanarà aquest token d'inici de sessió. - registerKey: Registra una clau de seguretat + registerSecurityKey: Registra una clau de seguretat step1: En primer lloc, instal·la una aplicació d'autenticació (com ara {a} o {b}) al dispositiu. step2: A continuació, escaneja el codi QR que es mostra en aquesta pantalla. diff --git a/locales/cs-CZ.yml b/locales/cs-CZ.yml index 8b502762fe..1fe16135e6 100644 --- a/locales/cs-CZ.yml +++ b/locales/cs-CZ.yml @@ -698,8 +698,8 @@ _time: minute: "Minut" hour: "Hodin" _2fa: - registerDevice: "Přidat zařízení" - registerKey: "Přidat bezpečnostní klíč" + registerTOTP: "Přidat zařízení" + registerSecurityKey: "Přidat bezpečnostní klíč" _weekday: sunday: "Neděle" monday: "Pondělí" diff --git a/locales/de-DE.yml b/locales/de-DE.yml index 5d0459395c..e8615b6f58 100644 --- a/locales/de-DE.yml +++ b/locales/de-DE.yml @@ -1371,8 +1371,8 @@ _tutorial: _2fa: alreadyRegistered: "Du hast bereits ein Gerät für Zwei-Faktor-Authentifizierung registriert." - registerDevice: "Neues Gerät registrieren" - registerKey: "Neuen Sicherheitsschlüssel registrieren" + registerTOTP: "Neues Gerät registrieren" + registerSecurityKey: "Neuen Sicherheitsschlüssel registrieren" step1: "Installiere zuerst eine Authentifizierungsapp (z.B. {a} oder {b}) auf deinem Gerät." step2: "Dann, scanne den angezeigten QR-Code mit deinem Gerät." diff --git a/locales/en-US.yml b/locales/en-US.yml index c560011e73..b2bc4e7146 100644 --- a/locales/en-US.yml +++ b/locales/en-US.yml @@ -1487,16 +1487,28 @@ _tutorial: step6_4: "Now go, explore, and have fun!" _2fa: alreadyRegistered: "You have already registered a 2-factor authentication device." - registerDevice: "Register a new device" - registerKey: "Register a security key" + registerTOTP: "Register authenticator app" step1: "First, install an authentication app (such as {a} or {b}) on your device." step2: "Then, scan the QR code displayed on this screen." + step2Click: "Clicking on this QR code will allow you to register 2FA to your security key or phone authenticator app." step2Url: "You can also enter this URL if you're using a desktop program:" + step3Title: "Enter an authentication code" step3: "Enter the token provided by your app to finish setup." step4: "From now on, any future login attempts will ask for such a login token." - securityKeyInfo: "Besides fingerprint or PIN authentication, you can also setup - authentication via hardware security keys that support FIDO2 to further secure - your account." + securityKeyNotSupported: "Your browser does not support security keys." + registerTOTPBeforeKey: "Please set up an authenticator app to register a security or pass key." + securityKeyInfo: "Besides fingerprint or PIN authentication, you can also setup authentication via hardware security keys that support FIDO2 to further secure your account." + chromePasskeyNotSupported: "Chrome passkeys are currently not supported." + registerSecurityKey: "Register a security or pass key" + securityKeyName: "Enter a key name" + tapSecurityKey: "Please follow your browser to register the security or pass key" + removeKey: "Remove security key" + removeKeyConfirm: "Really delete the {name} key?" + whyTOTPOnlyRenew: "The authenticator app cannot be removed as long as a security key is registered." + renewTOTP: "Reconfigure authenticator app" + renewTOTPConfirm: "This will cause verification codes from your previous app to stop working" + renewTOTPOk: "Reconfigure" + renewTOTPCancel: "Cancel" _permissions: "read:account": "View your account information" "write:account": "Edit your account information" @@ -2058,3 +2070,7 @@ _experiments: postImportsCaption: "Allows users to import their posts from past Calckey,\ \ Misskey, Mastodon, Akkoma, and Pleroma accounts. It may cause slowdowns during\ \ load if your queue is bottlenecked." + +_dialog: + charactersExceeded: "Max characters exceeded! Current: {current}/Limit: {max}" + charactersBelow: "Not enough characters! Current: {current}/Limit: {min}" diff --git a/locales/es-ES.yml b/locales/es-ES.yml index 2dc40f3597..a4016b7bb7 100644 --- a/locales/es-ES.yml +++ b/locales/es-ES.yml @@ -1331,8 +1331,8 @@ _tutorial: step6_4: "¡Ahora ve, explora y diviértete!" _2fa: alreadyRegistered: "Ya has completado la configuración." - registerDevice: "Registrar dispositivo" - registerKey: "Registrar clave" + registerTOTP: "Registrar dispositivo" + registerSecurityKey: "Registrar clave" step1: "Primero, instale en su dispositivo la aplicación de autenticación {a} o\ \ {b} u otra." step2: "Luego, escanee con la aplicación el código QR mostrado en pantalla." diff --git a/locales/fr-FR.yml b/locales/fr-FR.yml index dad0956880..2e6b1400f9 100644 --- a/locales/fr-FR.yml +++ b/locales/fr-FR.yml @@ -1262,8 +1262,8 @@ _tutorial: step6_4: "Maintenant, allez-y, explorez et amusez-vous !" _2fa: alreadyRegistered: "Configuration déjà achevée." - registerDevice: "Ajouter un nouvel appareil" - registerKey: "Enregistrer une clef" + registerTOTP: "Ajouter un nouvel appareil" + registerSecurityKey: "Enregistrer une clef" step1: "Tout d'abord, installez une application d'authentification, telle que {a}\ \ ou {b}, sur votre appareil." step2: "Ensuite, scannez le code QR affiché sur l’écran." diff --git a/locales/id-ID.yml b/locales/id-ID.yml index f9859c2c72..17bebe99cf 100644 --- a/locales/id-ID.yml +++ b/locales/id-ID.yml @@ -1254,8 +1254,8 @@ _tutorial: step7_3: "Semoga berhasil dan bersenang-senanglah! \U0001F680" _2fa: alreadyRegistered: "Kamu telah mendaftarkan perangkat otentikasi dua faktor." - registerDevice: "Daftarkan perangkat baru" - registerKey: "Daftarkan kunci keamanan baru" + registerTOTP: "Daftarkan perangkat baru" + registerSecurityKey: "Daftarkan kunci keamanan baru" step1: "Pertama, pasang aplikasi otentikasi (seperti {a} atau {b}) di perangkat\ \ kamu." step2: "Lalu, pindai kode QR yang ada di layar." diff --git a/locales/it-IT.yml b/locales/it-IT.yml index 44434eb264..7e39e7746c 100644 --- a/locales/it-IT.yml +++ b/locales/it-IT.yml @@ -1139,7 +1139,7 @@ _tutorial: Questo però lo fa! È un po' complicato, ma ci riuscirete in poco tempo" step6_4: "Ora andate, esplorate e divertitevi!" _2fa: - registerDevice: "Aggiungi dispositivo" + registerTOTP: "Aggiungi dispositivo" _permissions: "read:account": "Visualizzare le informazioni dell'account" "write:account": "Modificare le informazioni dell'account" diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 0ddde1a139..29462842da 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -1314,14 +1314,28 @@ _tutorial: step6_4: "これで完了です。お楽しみください!" _2fa: alreadyRegistered: "既に設定は完了しています。" - registerDevice: "デバイスを登録" - registerKey: "キーを登録" + registerTOTP: "認証アプリの設定を開始" step1: "まず、{a}や{b}などの認証アプリをお使いのデバイスにインストールします。" step2: "次に、表示されているQRコードをアプリでスキャンします。" - step2Url: "デスクトップアプリでは次のURLを入力します:" - step3: "アプリに表示されているトークンを入力して完了です。" - step4: "これからログインするときも、同じようにトークンを入力します。" - securityKeyInfo: "FIDO2をサポートするハードウェアセキュリティキーもしくは端末の指紋認証やPINを使用してログインするように設定できます。" + step2Click: "QRコードをクリックすると、お使いの端末にインストールされている認証アプリやキーリングに登録できます。" + step2Url: "デスクトップアプリでは次のURIを入力します:" + step3Title: "確認コードを入力" + step3: "アプリに表示されている確認コード(トークン)を入力して完了です。" + step4: "これからログインするときも、同じように確認コードを入力します。" + securityKeyNotSupported: "お使いのブラウザはセキュリティキーに対応していません。" + registerTOTPBeforeKey: "セキュリティキー・パスキーを登録するには、まず認証アプリの設定を行なってください。" + securityKeyInfo: "FIDO2をサポートするハードウェアセキュリティキー、端末の生体認証やPINロック、パスキーといった、WebAuthn由来の鍵を登録します。" + chromePasskeyNotSupported: "Chromeのパスキーは現在サポートしていません。" + registerSecurityKey: "セキュリティキー・パスキーを登録する" + securityKeyName: "キーの名前を入力" + tapSecurityKey: "ブラウザの指示に従い、セキュリティキーやパスキーを登録してください" + removeKey: "セキュリティキーを削除" + removeKeyConfirm: "{name}を削除しますか?" + whyTOTPOnlyRenew: "セキュリティキーが登録されている場合、認証アプリの設定は解除できません。" + renewTOTP: "認証アプリを再設定" + renewTOTPConfirm: "今までの認証アプリの確認コードは使用できなくなります" + renewTOTPOk: "再設定する" + renewTOTPCancel: "やめておく" _permissions: "read:account": "アカウントの情報を見る" "write:account": "アカウントの情報を変更する" @@ -1882,7 +1896,7 @@ sendModMail: モデレーションノートを送る deleted: 削除済み editNote: 投稿を編集 edited: 編集済み -signupsDisabled: +signupsDisabled: 現在、このサーバーでは新規登録が一般開放されていません。招待コードをお持ちの場合には、以下の欄に入力してください。招待コードをお持ちでない場合にも、新規登録を開放している他のサーバーには入れますよ! findOtherInstance: 他のサーバーを探す newer: 新しい投稿 @@ -1898,3 +1912,6 @@ antennasDesc: "アンテナでは指定した条件に合致する投稿が表 expandOnNoteClickDesc: オフの場合、右クリックメニューか日付をクリックすることで開けます。 expandOnNoteClick: クリックで投稿の詳細を開く clipsDesc: クリップは分類と共有ができるブックマークです。各投稿のメニューからクリップを作成できます。 +_dialog: + charactersExceeded: "最大文字数を超えています! 現在 {current} / 制限 {max}" + charactersBelow: "最小文字数を下回っています! 現在 {current} / 制限 {min}" diff --git a/locales/ko-KR.yml b/locales/ko-KR.yml index 7143cf2f98..2c8e548bde 100644 --- a/locales/ko-KR.yml +++ b/locales/ko-KR.yml @@ -1179,8 +1179,8 @@ _time: day: "일" _2fa: alreadyRegistered: "이미 설정이 완료되었습니다." - registerDevice: "디바이스 등록" - registerKey: "키를 등록" + registerTOTP: "디바이스 등록" + registerSecurityKey: "키를 등록" step1: "먼저, {a}나 {b}등의 인증 앱을 사용 중인 디바이스에 설치합니다." step2: "그 후, 표시되어 있는 QR코드를 앱으로 스캔합니다." step2Url: "데스크톱 앱에서는 다음 URL을 입력하세요:" diff --git a/locales/pl-PL.yml b/locales/pl-PL.yml index e2ca8b1db8..fa5ec88d8c 100644 --- a/locales/pl-PL.yml +++ b/locales/pl-PL.yml @@ -1260,8 +1260,8 @@ _tutorial: step6_4: "A teraz idź, odkrywaj i baw się dobrze!" _2fa: alreadyRegistered: "Zarejestrowałeś już urządzenie do uwierzytelniania dwuskładnikowego." - registerDevice: "Zarejestruj nowe urządzenie" - registerKey: "Zarejestruj klucz bezpieczeństwa" + registerTOTP: "Zarejestruj nowe urządzenie" + registerSecurityKey: "Zarejestruj klucz bezpieczeństwa" step1: "Najpierw, zainstaluj aplikację uwierzytelniającą (taką jak {a} lub {b}) na swoim urządzeniu." step2: "Następnie, zeskanuje kod QR z ekranu." diff --git a/locales/ru-RU.yml b/locales/ru-RU.yml index 01b21b0fcb..fe90f23d9b 100644 --- a/locales/ru-RU.yml +++ b/locales/ru-RU.yml @@ -1249,8 +1249,8 @@ _tutorial: step6_4: "Теперь идите, изучайте и развлекайтесь!" _2fa: alreadyRegistered: "Двухфакторная аутентификация уже настроена." - registerDevice: "Зарегистрируйте ваше устройство" - registerKey: "Зарегистрировать ключ" + registerTOTP: "Зарегистрируйте ваше устройство" + registerSecurityKey: "Зарегистрировать ключ" step1: "Прежде всего, установите на устройство приложение для аутентификации, например,\ \ {a} или {b}." step2: "Далее отсканируйте отображаемый QR-код при помощи приложения." diff --git a/locales/sk-SK.yml b/locales/sk-SK.yml index 462f34ed2d..dce23d7558 100644 --- a/locales/sk-SK.yml +++ b/locales/sk-SK.yml @@ -1196,8 +1196,8 @@ _tutorial: step6_4: "Now go, explore, and have fun!" _2fa: alreadyRegistered: "Už ste zaregistrovali 2-faktorové autentifikačné zariadenie." - registerDevice: "Registrovať nové zariadenie" - registerKey: "Registrovať bezpečnostný kľúč" + registerTOTP: "Registrovať nové zariadenie" + registerSecurityKey: "Registrovať bezpečnostný kľúč" step1: "Najprv si nainštalujte autentifikačnú aplikáciu (napríklad {a} alebo {b}) na svoje zariadenie." step2: "Potom, naskenujte QR kód zobrazený na obrazovke." step2Url: "Do aplikácie zadajte nasledujúcu URL adresu:" diff --git a/locales/uk-UA.yml b/locales/uk-UA.yml index 549dce666c..712c0fd03e 100644 --- a/locales/uk-UA.yml +++ b/locales/uk-UA.yml @@ -959,7 +959,7 @@ _tutorial: step6_3: "Кожен сервер працює по-своєму, і не на всіх серверах працює Calckey. Але цей працює! Це трохи складно, але ви швидко розберетеся" step6_4: "Тепер ідіть, вивчайте і розважайтеся!" _2fa: - registerKey: "Зареєструвати новий ключ безпеки" + registerSecurityKey: "Зареєструвати новий ключ безпеки" _permissions: "read:account": "Переглядати дані профілю" "write:account": "Змінити дані акаунту" diff --git a/locales/vi-VN.yml b/locales/vi-VN.yml index 4b254fe943..ddd79084fc 100644 --- a/locales/vi-VN.yml +++ b/locales/vi-VN.yml @@ -1201,8 +1201,8 @@ _tutorial: step6_4: "Now go, explore, and have fun!" _2fa: alreadyRegistered: "Bạn đã đăng ký thiết bị xác minh 2 bước." - registerDevice: "Đăng ký một thiết bị" - registerKey: "Đăng ký một mã bảo vệ" + registerTOTP: "Đăng ký một thiết bị" + registerSecurityKey: "Đăng ký một mã bảo vệ" step1: "Trước tiên, hãy cài đặt một ứng dụng xác minh (chẳng hạn như {a} hoặc {b}) trên thiết bị của bạn." step2: "Sau đó, quét mã QR hiển thị trên màn hình này." step2Url: "Bạn cũng có thể nhập URL này nếu sử dụng một chương trình máy tính:" diff --git a/locales/zh-CN.yml b/locales/zh-CN.yml index 5359cb6efb..a6c9ec5a1e 100644 --- a/locales/zh-CN.yml +++ b/locales/zh-CN.yml @@ -1210,8 +1210,8 @@ _tutorial: step6_4: "现在去学习并享受乐趣!" _2fa: alreadyRegistered: "此设备已被注册" - registerDevice: "注册设备" - registerKey: "注册密钥" + registerTOTP: "注册设备" + registerSecurityKey: "注册密钥" step1: "首先,在您的设备上安装验证应用,例如{a}或{b}。" step2: "然后,扫描屏幕上显示的二维码。" step2Url: "在桌面应用程序中输入以下URL:" diff --git a/locales/zh-TW.yml b/locales/zh-TW.yml index 61b5afbe63..3b552ffd29 100644 --- a/locales/zh-TW.yml +++ b/locales/zh-TW.yml @@ -1219,8 +1219,8 @@ _tutorial: step6_4: "現在開始探索吧!" _2fa: alreadyRegistered: "你已註冊過一個雙重認證的裝置。" - registerDevice: "註冊裝置" - registerKey: "註冊鍵" + registerTOTP: "註冊裝置" + registerSecurityKey: "註冊鍵" step1: "首先,在您的設備上安裝二步驗證程式,例如{a}或{b}。" step2: "然後,掃描螢幕上的QR code。" step2Url: "在桌面版應用中,請輸入以下的URL:" diff --git a/package.json b/package.json index b2c90463b8..42a18a33c1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "calckey", - "version": "14.0.0-dev46", + "version": "14.0.0-dev51", "codename": "aqua", "repository": { "type": "git", diff --git a/packages/backend/package.json b/packages/backend/package.json index cf04b3bf72..2a19b916cf 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -101,6 +101,7 @@ "nsfwjs": "2.4.2", "oauth": "^0.10.0", "os-utils": "0.0.14", + "otpauth": "^9.1.2", "parse5": "7.1.2", "pg": "8.11.0", "private-ip": "2.3.4", @@ -123,7 +124,6 @@ "semver": "7.5.1", "sharp": "0.32.1", "sonic-channel": "^1.3.1", - "speakeasy": "2.0.0", "stringz": "2.1.0", "summaly": "2.7.0", "syslog-pro": "1.0.0", @@ -181,7 +181,6 @@ "@types/semver": "7.5.0", "@types/sharp": "0.31.1", "@types/sinonjs__fake-timers": "8.1.2", - "@types/speakeasy": "2.0.7", "@types/tinycolor2": "1.4.3", "@types/tmp": "0.2.3", "@types/uuid": "8.3.4", diff --git a/packages/backend/src/server/api/endpoints.ts b/packages/backend/src/server/api/endpoints.ts index e0b513298d..2cb3b30d10 100644 --- a/packages/backend/src/server/api/endpoints.ts +++ b/packages/backend/src/server/api/endpoints.ts @@ -174,6 +174,7 @@ import * as ep___i_2fa_keyDone from "./endpoints/i/2fa/key-done.js"; import * as ep___i_2fa_passwordLess from "./endpoints/i/2fa/password-less.js"; import * as ep___i_2fa_registerKey from "./endpoints/i/2fa/register-key.js"; import * as ep___i_2fa_register from "./endpoints/i/2fa/register.js"; +import * as ep___i_2fa_updateKey from "./endpoints/i/2fa/update-key.js"; import * as ep___i_2fa_removeKey from "./endpoints/i/2fa/remove-key.js"; import * as ep___i_2fa_unregister from "./endpoints/i/2fa/unregister.js"; import * as ep___i_apps from "./endpoints/i/apps.js"; @@ -528,6 +529,7 @@ const eps = [ ["i/2fa/password-less", ep___i_2fa_passwordLess], ["i/2fa/register-key", ep___i_2fa_registerKey], ["i/2fa/register", ep___i_2fa_register], + ["i/2fa/update-key", ep___i_2fa_updateKey], ["i/2fa/remove-key", ep___i_2fa_removeKey], ["i/2fa/unregister", ep___i_2fa_unregister], ["i/apps", ep___i_apps], diff --git a/packages/backend/src/server/api/endpoints/i/2fa/done.ts b/packages/backend/src/server/api/endpoints/i/2fa/done.ts index 1e9892f03b..05d57d2821 100644 --- a/packages/backend/src/server/api/endpoints/i/2fa/done.ts +++ b/packages/backend/src/server/api/endpoints/i/2fa/done.ts @@ -1,6 +1,7 @@ -import * as speakeasy from "speakeasy"; +import { publishMainStream } from "@/services/stream.js"; +import * as OTPAuth from "otpauth"; import define from "../../../define.js"; -import { UserProfiles } from "@/models/index.js"; +import { Users, UserProfiles } from "@/models/index.js"; export const meta = { requireCredential: true, @@ -25,13 +26,14 @@ export default define(meta, paramDef, async (ps, user) => { throw new Error("二段階認証の設定が開始されていません"); } - const verified = (speakeasy as any).totp.verify({ - secret: profile.twoFactorTempSecret, - encoding: "base32", - token: token, + const delta = OTPAuth.TOTP.validate({ + secret: OTPAuth.Secret.fromBase32(profile.twoFactorTempSecret), + digits: 6, + token, + window: 1, }); - if (!verified) { + if (delta === null) { throw new Error("not verified"); } @@ -39,4 +41,11 @@ export default define(meta, paramDef, async (ps, user) => { twoFactorSecret: profile.twoFactorTempSecret, twoFactorEnabled: true, }); + + const iObj = await Users.pack(user.id, user, { + detail: true, + includeSecrets: true, + }); + + publishMainStream(user.id, "meUpdated", iObj); }); diff --git a/packages/backend/src/server/api/endpoints/i/2fa/key-done.ts b/packages/backend/src/server/api/endpoints/i/2fa/key-done.ts index f0581de4b4..34660c6f2e 100644 --- a/packages/backend/src/server/api/endpoints/i/2fa/key-done.ts +++ b/packages/backend/src/server/api/endpoints/i/2fa/key-done.ts @@ -28,7 +28,7 @@ export const paramDef = { attestationObject: { type: "string" }, password: { type: "string" }, challengeId: { type: "string" }, - name: { type: "string" }, + name: { type: "string", minLength: 1, maxLength: 30 }, }, required: [ "clientDataJSON", diff --git a/packages/backend/src/server/api/endpoints/i/2fa/password-less.ts b/packages/backend/src/server/api/endpoints/i/2fa/password-less.ts index 11b2e9a2e3..b9f3426804 100644 --- a/packages/backend/src/server/api/endpoints/i/2fa/password-less.ts +++ b/packages/backend/src/server/api/endpoints/i/2fa/password-less.ts @@ -1,10 +1,20 @@ import define from "../../../define.js"; -import { UserProfiles } from "@/models/index.js"; +import { Users, UserProfiles, UserSecurityKeys } from "@/models/index.js"; +import { publishMainStream } from "@/services/stream.js"; +import { ApiError } from "../../../error.js"; export const meta = { requireCredential: true, secure: true, + + errors: { + noKey: { + message: "No security key.", + code: "NO_SECURITY_KEY", + id: "f9c54d7f-d4c2-4d3c-9a8g-a70daac86512", + }, + }, } as const; export const paramDef = { @@ -16,7 +26,36 @@ export const paramDef = { } as const; export default define(meta, paramDef, async (ps, user) => { + if (ps.value === true) { + // セキュリティキーがなければパスワードレスを有効にはできない + const keyCount = await UserSecurityKeys.count({ + where: { + userId: user.id, + }, + select: { + id: true, + name: true, + lastUsed: true, + }, + }); + + if (keyCount === 0) { + await UserProfiles.update(user.id, { + usePasswordLessLogin: false, + }); + + throw new ApiError(meta.errors.noKey); + } + } + await UserProfiles.update(user.id, { usePasswordLessLogin: ps.value, }); + + const iObj = await Users.pack(user.id, user, { + detail: true, + includeSecrets: true, + }); + + publishMainStream(user.id, "meUpdated", iObj); }); diff --git a/packages/backend/src/server/api/endpoints/i/2fa/register.ts b/packages/backend/src/server/api/endpoints/i/2fa/register.ts index 533035bc91..cf391ca2fd 100644 --- a/packages/backend/src/server/api/endpoints/i/2fa/register.ts +++ b/packages/backend/src/server/api/endpoints/i/2fa/register.ts @@ -1,4 +1,4 @@ -import * as speakeasy from "speakeasy"; +import * as OTPAuth from "otpauth"; import * as QRCode from "qrcode"; import config from "@/config/index.js"; import { UserProfiles } from "@/models/index.js"; @@ -30,25 +30,24 @@ export default define(meta, paramDef, async (ps, user) => { } // Generate user's secret key - const secret = speakeasy.generateSecret({ - length: 32, - }); + const secret = new OTPAuth.Secret(); await UserProfiles.update(user.id, { twoFactorTempSecret: secret.base32, }); // Get the data URL of the authenticator URL - const url = speakeasy.otpauthURL({ - secret: secret.base32, - encoding: "base32", + const totp = new OTPAuth.TOTP({ + secret, + digits: 6, label: user.username, issuer: config.host, }); - const dataUrl = await QRCode.toDataURL(url); + const url = totp.toString(); + const qr = await QRCode.toDataURL(url); return { - qr: dataUrl, + qr, url, secret: secret.base32, label: user.username, diff --git a/packages/backend/src/server/api/endpoints/i/2fa/remove-key.ts b/packages/backend/src/server/api/endpoints/i/2fa/remove-key.ts index 862c971e75..d91c8f214b 100644 --- a/packages/backend/src/server/api/endpoints/i/2fa/remove-key.ts +++ b/packages/backend/src/server/api/endpoints/i/2fa/remove-key.ts @@ -34,6 +34,24 @@ export default define(meta, paramDef, async (ps, user) => { id: ps.credentialId, }); + // 使われているキーがなくなったらパスワードレスログインをやめる + const keyCount = await UserSecurityKeys.count({ + where: { + userId: user.id, + }, + select: { + id: true, + name: true, + lastUsed: true, + }, + }); + + if (keyCount === 0) { + await UserProfiles.update(me.id, { + usePasswordLessLogin: false, + }); + } + // Publish meUpdated event publishMainStream( user.id, diff --git a/packages/backend/src/server/api/endpoints/i/2fa/unregister.ts b/packages/backend/src/server/api/endpoints/i/2fa/unregister.ts index 57d57ff65a..54f1422d4c 100644 --- a/packages/backend/src/server/api/endpoints/i/2fa/unregister.ts +++ b/packages/backend/src/server/api/endpoints/i/2fa/unregister.ts @@ -1,5 +1,6 @@ +import { publishMainStream } from "@/services/stream.js"; import define from "../../../define.js"; -import { UserProfiles } from "@/models/index.js"; +import { Users, UserProfiles } from "@/models/index.js"; import { comparePassword } from "@/misc/password.js"; export const meta = { @@ -29,5 +30,13 @@ export default define(meta, paramDef, async (ps, user) => { await UserProfiles.update(user.id, { twoFactorSecret: null, twoFactorEnabled: false, + usePasswordLessLogin: false, }); + + const iObj = await Users.pack(user.id, user, { + detail: true, + includeSecrets: true, + }); + + publishMainStream(user.id, "meUpdated", iObj); }); diff --git a/packages/backend/src/server/api/endpoints/i/2fa/update-key.ts b/packages/backend/src/server/api/endpoints/i/2fa/update-key.ts new file mode 100644 index 0000000000..7587ec780e --- /dev/null +++ b/packages/backend/src/server/api/endpoints/i/2fa/update-key.ts @@ -0,0 +1,58 @@ +import { publishMainStream } from "@/services/stream.js"; +import define from "../../../define.js"; +import { Users, UserSecurityKeys } from "@/models/index.js"; +import { ApiError } from "../../../error.js"; + +export const meta = { + requireCredential: true, + + secure: true, + + errors: { + noSuchKey: { + message: "No such key.", + code: "NO_SUCH_KEY", + id: "f9c5467f-d492-4d3c-9a8g-a70dacc86512", + }, + + accessDenied: { + message: "You do not have edit privilege of the channel.", + code: "ACCESS_DENIED", + id: "1fb7cb09-d46a-4fff-b8df-057708cce513", + }, + }, +} as const; + +export const paramDef = { + type: "object", + properties: { + name: { type: "string", minLength: 1, maxLength: 30 }, + credentialId: { type: "string" }, + }, + required: ["name", "credentialId"], +} as const; + +export default define(meta, paramDef, async (ps, user) => { + const key = await UserSecurityKeys.findOneBy({ + id: ps.credentialId, + }); + + if (key == null) { + throw new ApiError(meta.errors.noSuchKey); + } + + if (key.userId !== user.id) { + throw new ApiError(meta.errors.accessDenied); + } + + await UserSecurityKeys.update(key.id, { + name: ps.name, + }); + + const iObj = await Users.pack(user.id, user, { + detail: true, + includeSecrets: true, + }); + + publishMainStream(user.id, "meUpdated", iObj); +}); diff --git a/packages/backend/src/server/api/private/signin.ts b/packages/backend/src/server/api/private/signin.ts index ef5b137813..06d801a953 100644 --- a/packages/backend/src/server/api/private/signin.ts +++ b/packages/backend/src/server/api/private/signin.ts @@ -1,5 +1,5 @@ import type Koa from "koa"; -import * as speakeasy from "speakeasy"; +import * as OTPAuth from "otpauth"; import signin from "../common/signin.js"; import config from "@/config/index.js"; import { @@ -136,14 +136,18 @@ export default async (ctx: Koa.Context) => { return; } - const verified = (speakeasy as any).totp.verify({ - secret: profile.twoFactorSecret, - encoding: "base32", - token: token, - window: 2, + if (profile.twoFactorSecret == null) { + throw new Error("Attempted 2FA signin without 2FA enabled."); + } + + const delta = OTPAuth.TOTP.validate({ + secret: OTPAuth.Secret.fromBase32(profile.twoFactorSecret), + digits: 6, + token, + window: 1, }); - if (verified) { + if (delta != null) { signin(ctx, user); return; } else { diff --git a/packages/calckey-js/src/api.types.ts b/packages/calckey-js/src/api.types.ts index 9f16056da1..626bdaad02 100644 --- a/packages/calckey-js/src/api.types.ts +++ b/packages/calckey-js/src/api.types.ts @@ -725,6 +725,7 @@ export type Endpoints = { "i/2fa/password-less": { req: TODO; res: TODO }; "i/2fa/register-key": { req: TODO; res: TODO }; "i/2fa/register": { req: TODO; res: TODO }; + "i/2fa/update-key": { req: TODO; res: TODO }; "i/2fa/remove-key": { req: TODO; res: TODO }; "i/2fa/unregister": { req: TODO; res: TODO }; diff --git a/packages/client/src/components/MkDialog.vue b/packages/client/src/components/MkDialog.vue index 20f0b9a74d..f870005eff 100644 --- a/packages/client/src/components/MkDialog.vue +++ b/packages/client/src/components/MkDialog.vue @@ -53,12 +53,15 @@ > -
+
+ +
+