Class TwoFactorRecoveryCrypto
Password-based authenticated encryption for the local 2FA recovery-code payload.
Why the account password and not a machine-bound key. The obvious alternative is a key sealed to the machine (DPAPI, keychain, a file next to the payload). That protects the codes from another local process, but it dies with the installation — and a reinstall, a new machine or a wiped profile is precisely the situation in which a player reaches for recovery codes. A machine-bound key would therefore trade a confidentiality problem for an availability one, and locking a player out of their own account is the worse failure of the two. The account password is in hand at exactly the two moments that matter — when the codes are written at 2FA setup, and when the player legitimately asks to read them back — and it is the one secret that survives a reinstall.
Primitives. Argon2id and AES-256-GCM, both from the BouncyCastle build this
project already ships (Assets/Dependencies/BouncyCastle.Cryptography.dll) and already
depends on for all of its other crypto. No new dependency. BouncyCastle rather than
System.Security.Cryptography.AesGcm for the same reason
CryptoHelper.EncryptAES uses it: it behaves identically on every Unity player target,
where the BCL AEAD types are not uniformly available.
Why Argon2id rather than the PBKDF2 the server uses for recovery-code hashing.
CryptoHelper.TwoFactor.HashRecoveryCode is PBKDF2-HMAC-SHA256 at 600k iterations
because it runs server-side, once per verification, against a high-entropy random code. This
runs client-side, twice per account lifetime, against a human-chosen password, and the
attacker is someone who already has the file. Memory-hardness is the property that matters
there, and the parameters are recorded in the envelope so they can be raised later without
stranding files written by an older client.
public static class TwoFactorRecoveryCrypto
- Inheritance
-
TwoFactorRecoveryCrypto
- Inherited Members
Methods
Encrypt(string, string)
Encrypts plaintext under a key derived from password.
public static byte[] Encrypt(string password, string plaintext)
Parameters
passwordstringThe account password. Never stored, never logged.
plaintextstringThe recovery payload to protect.
Returns
- byte[]
A self-describing envelope: salt, nonce, KDF parameters, ciphertext and tag.
Remarks
The whole header — including the KDF parameters — is fed to GCM as additional authenticated data. Without that, an attacker holding the file could rewrite the iteration and memory costs down to 1 and hand the weakened file back to the client, which would then derive the key cheaply on their behalf. Binding the parameters means any such edit fails the tag check instead.
Exceptions
- ArgumentException
The password or plaintext is empty.
LooksLikeEnvelope(byte[])
True if blob begins with the envelope magic.
public static bool LooksLikeEnvelope(byte[] blob)
Parameters
blobbyte[]Candidate file contents.
Returns
- bool
trueif this looks like an envelope;falsefor anything else, including the legacy plaintext payload.
TryDecrypt(string, byte[], out string)
Attempts to open an envelope produced by Encrypt(string, string).
public static TwoFactorRecoveryReadResult TryDecrypt(string password, byte[] blob, out string plaintext)
Parameters
passwordstringThe account password.
blobbyte[]The stored file contents.
plaintextstringReceives the recovered payload on success;
nullotherwise.
Returns
- TwoFactorRecoveryReadResult
Why the read succeeded or failed. See TwoFactorRecoveryReadResult.
Remarks
This never throws on bad input and never deletes anything. A failure here is a fact about this attempt, not a verdict on the file.