Восстановление аккаунта SafePass, версия 1
Восстановление меняет серверные учётные данные OPAQUE. Записи сейфа остаются зашифрованными и не переписываются. Фраза из 24 слов остаётся у клиента. Из неё выводится регистрируемый в API ключ Ed25519, а также локально раскрывается привязанный к аккаунту конверт ключа сейфа SPRE.
Регистрация и ротация ключа восстановления
Аутентифицированный клиент вызывает POST /v1/account/recovery-envelope/challenge. API возвращает случайные 32-байтовые идентификатор вызова и nonce, а также текущий открытый ключ, если он зарегистрирован. Вызов действует пять минут и используется один раз.
Клиент выводит новый открытый ключ Ed25519 из фразы и подписывает последовательность байтов:
"SafePass account recovery envelope v1\0"
|| account_id_ascii[36]
|| challenge_id[32]
|| challenge_nonce[32]
|| new_recovery_public_key[32]
|| SHA-256(recovery_envelope[81])Подпись создаётся ключом Ed25519, выведенным из фразы. При первой регистрации запрос подписывает новый ключ, подтверждая владение фразой. При ротации прежний ключ подписывает новый ключ и конверт, поэтому украденного токена доступа недостаточно для подмены. PUT /v1/account/recovery-envelope проверяет доказательство и атомарно сохраняет шифротекст и открытый ключ, погашая вызов. API не получает фразу или ключ сейфа.
Сброс пароля OPAQUE
Клиент начинает новую регистрацию OPAQUE существующего аккаунта и отправляет запрос в POST /v1/auth/recovery/start. API возвращает ответ регистрации, зарегистрированный открытый ключ и новый одноразовый вызов сроком на пять минут. Для неизвестных аккаунтов и аккаунтов без зарегистрированного ключа ответ имеет ту же форму, но содержит случайный ключ-приманку. Начало и завершение восстановления используют общий лимит анонимной аутентификации: 20 запросов в минуту на IP.
Клиент выводит открытый ключ из фразы и сверяет его с ответом. Затем он готовит финальные данные регистрации для запрошенного аккаунта и подписывает:
"SafePass account recovery reset v1\0"
|| account_id_ascii[36]
|| challenge_id[32]
|| challenge_nonce[32]
|| SHA-256(opaque_registration_upload)crypto_core добавляет разделитель домена подписи восстановления. Хеш финальных данных привязывает доказательство к конкретным новым учётным данным и исключает использование перехваченной подписи для регистрации другого пароля.
POST /v1/auth/recovery/finish проверяет подпись зарегистрированным открытым ключом и выводит серверный файл пароля OPAQUE. В одной транзакции PostgreSQL он заменяет этот файл, погашает все открытые вызовы и отзывает все семейства токенов аккаунта. Новая пара токенов выдаётся после фиксации транзакции. Недействительные доказательства, истёкшие вызовы, неизвестные аккаунты и повторы дают одинаковый отказ.
Отзыв прекращает действие JWT с sid сразу. Старый JWT без sid может работать до истечения 15-минутного срока. Сброс не меняет мастер-пароль локального устройства. Для локального восстановления пользователь раскрывает ключ из SPRE на клиенте и выбирает новый локальный пароль.
На привязанном устройстве открытый сейф и записи сохраняются. На новом устройстве с пустым сейфом клиент загружает конверт, проверяет и раскрывает его фразой аккаунта, шифрует общий ключ выбранным локальным мастер-паролем и синхронизирует зашифрованные записи. Непривязанный сейф с записями нельзя перезаписать этим способом.
Хранимые данные и совместимость
Миграция 006_account_recovery_challenges.sql добавляет поле открытого ключа длиной 32 байта и таблицу с идентификаторами вызовов, публичными nonce, назначением, сроком, одноразовым состоянием и снимком ключа для проверки. Просроченные строки удаляются при очистке. Эти данные не содержат пароль, мнемонику, закрытый ключ или открытые данные сейфа.
У конвертов, созданных до v1, может не быть зарегистрированного открытого ключа. Для восстановления пароля нужно сначала зарегистрировать его с аутентифицированного устройства. Прежний зашифрованный конверт остаётся недоступен для расшифровки API.