Backup format
Version 1. A backup is meant to outlive the app, so everything needed to open one is documented here.
The file
A single JSON document. All binary values are base64.
{
"format": "vault2fa-backup",
"version": 1,
"created": "2026-09-10T11:00:58Z",
"kdf": {
"name": "pbkdf2-hmac-sha256",
"iterations": 600000,
"salt": "<16 random bytes>"
},
"cipher": {
"name": "aes-256-gcm",
"nonce": "<12 random bytes>"
},
"ciphertext": "<AES-GCM output with the 16-byte tag appended>"
}
Opening it
| Step | Detail |
|---|---|
| 1. Derive the key | PBKDF2-HMAC-SHA256 over the UTF-8 passphrase with kdf.salt and kdf.iterations, 32-byte output. |
| 2. Split the ciphertext | Last 16 bytes are the GCM tag; everything before is the encrypted body. |
| 3. Decrypt | AES-256-GCM with the key, cipher.nonce, no additional authenticated data. |
| 4. Parse | The plaintext is the JSON below. |
A wrong passphrase and a tampered file are indistinguishable — both fail the GCM tag check.
The plaintext
{
"app": "Vault2FA",
"exported": "2026-09-10T11:00:58Z",
"accounts": [
{
"uri": "otpauth://totp/GitHub:you%40example.com?secret=…&issuer=GitHub&algorithm=SHA1&digits=6&period=30",
"isFavorite": true,
"notes": "",
"sortIndex": 0,
"createdAt": "2026-09-10T02:01:12Z"
}
]
}
uri is a standard Key URI, so the accounts can be imported into any authenticator. The other fields are Vault2FA's own metadata.
Reference implementation
The decryption in about forty lines of Python, using only the standard library plus cryptography for AES-GCM:
import base64, hashlib, json, getpass
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
env = json.load(open("vault2fa-backup-2026-09-10.json"))
assert env["format"] == "vault2fa-backup" and env["version"] == 1
key = hashlib.pbkdf2_hmac(
"sha256",
getpass.getpass("Passphrase: ").encode(),
base64.b64decode(env["kdf"]["salt"]),
env["kdf"]["iterations"],
dklen=32,
)
plaintext = AESGCM(key).decrypt(
base64.b64decode(env["cipher"]["nonce"]),
base64.b64decode(env["ciphertext"]),
None,
)
for account in json.loads(plaintext)["accounts"]:
print(account["uri"])
Why these choices
PBKDF2 rather than Argon2 or scrypt because it is what iOS provides natively. A backup format should not depend on a third-party library still being maintained in ten years. 600,000 iterations is above the OWASP 2023 recommendation for PBKDF2-HMAC-SHA256. AES-256-GCM provides authentication as well as confidentiality, so a damaged or altered file is detected rather than decrypted into garbage.