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 @@
>