Table of Contents

Class TwoFactorRecoveryCrypto

Namespace
FishMMO.Client.Security
Assembly
FishMMO.Client.dll

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

password string

The account password. Never stored, never logged.

plaintext string

The 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

blob byte[]

Candidate file contents.

Returns

bool

true if this looks like an envelope; false for 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

password string

The account password.

blob byte[]

The stored file contents.

plaintext string

Receives the recovered payload on success; null otherwise.

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.