Das Format der Sicherungsdatei ist veröffentlicht, damit du deine Sicherungen auch ohne FairAuth öffnen kannst. Die Beschreibung gibt es auf Englisch und Deutsch.
FairAuth-Sicherungsdatei (Format Version 1)
Stand: 2026-09-25. Dieses Format ist offen dokumentiert, damit du deine Codes jederzeit auch ohne FairAuth lesen und in jede andere App mitnehmen kannst. Die Beschreibung ist vollständig: Mit ihr und einer libsodium-Anbindung lässt sich jede FairAuth-Sicherung öffnen. Am Ende steht ein Beispielprogramm.
Herausgeber: BYTEPOTATO UG (haftungsbeschränkt).
Überblick
Eine Sicherungsdatei hat die Endung .fairauth. Sie besteht aus drei Teilen:
- einem festen Vorspann von 16 Byte,
- einem lesbaren Kopf im JSON-Format mit allen Parametern,
- dem verschlüsselten Inhalt als Folge von Nachrichten.
Verschlüsselung in zwei Stufen:
- Ein zufälliger Datenschlüssel (32 Byte) verschlüsselt den Inhalt mit XChaCha20-Poly1305 im Streaming-Verfahren von libsodium (
crypto_secretstream_xchacha20poly1305). - Der Datenschlüssel liegt im Kopf, verschlüsselt mit einem Schlüssel, der aus deinem Sicherungspasswort abgeleitet wird: Argon2id nach RFC 9106, dann XChaCha20-Poly1305 (
crypto_aead_xchacha20poly1305_ietf).
Ohne das Passwort lässt sich die Datei nicht öffnen. Auch BYTEPOTATO UG (haftungsbeschränkt) kann das nicht.
Alle Ganzzahlen sind little-endian. Base64 ist Standard-Base64 nach RFC 4648 Abschnitt 4 mit Auffüllung.
1. Vorspann (16 Byte)
| Offset | Größe | Inhalt |
|---|---|---|
| 0 | 8 | ASCII FAIRAUTH |
| 8 | 1 | Containerversion, 1 |
| 9 | 3 | reserviert: Schreiber setzen alle drei Byte auf 0, Leser ignorieren sie |
| 12 | 4 | Länge des Kopfes in Byte (u32), 2 bis 65.536 |
2. Kopf (JSON, UTF-8)
{
"format": "fairauth-backup",
"version": 1,
"created": "2026-09-25T10:15:00Z",
"kdf": {
"algorithm": "argon2id",
"version": 19,
"opsLimit": 3,
"memLimitBytes": 268435456,
"salt": "<Base64, 16 Byte>"
},
"keyWrap": {
"algorithm": "xchacha20poly1305-ietf",
"nonce": "<Base64, 24 Byte>",
"ciphertext": "<Base64, 48 Byte>"
},
"payload": {
"algorithm": "secretstream-xchacha20poly1305",
"header": "<Base64, 24 Byte>"
}
}
| Feld | Bedeutung |
|---|---|
kdf.version | Argon2-Version 19 (0x13), in libsodium crypto_pwhash_ALG_ARGON2ID13 |
kdf.opsLimit, kdf.memLimitBytes | FairAuth schreibt mindestens 3 und 268.435.456 (OPSLIMIT_MODERATE, MEMLIMIT_MODERATE). Leser akzeptieren opsLimit 2 bis 16 und memLimitBytes 67.108.864 bis 1.073.741.824 |
keyWrap.ciphertext | 32 Byte Datenschlüssel plus 16 Byte Tag |
payload.header | Kopf des secretstream (crypto_secretstream_xchacha20poly1305_HEADERBYTES = 24) |
Leser ignorieren unbekannte Felder im Kopf. Vorspann und Kopf enthalten drei Versionsangaben. FairAuth behandelt sie beim Lesen so:
| Angabe | FairAuth schreibt | Leser |
|---|---|---|
| Containerversion (Vorspann, Offset 8) | 1 | 1: weiterlesen. Größer als 1: Datei nicht öffnen, sondern auf ein Update hinweisen. 0: keine FairAuth-Sicherung |
version (Kopf) | 1 | 1: weiterlesen. Jede andere Ganzzahl: Datei nicht öffnen, sondern auf ein Update hinweisen. Fehlt version, ist es keine Ganzzahl oder ist format nicht fairauth-backup: keine FairAuth-Sicherung |
kdf.version (Kopf) | 19 | Pflichtfeld, muss genau 19 sein. Jeden anderen Wert und ein fehlendes Feld: Datei als beschädigt ablehnen, ohne Hinweis auf ein Update |
Das Feld schema im manifest (Abschnitt 5.1) ist keine dieser Angaben: FairAuth schreibt dort 1 und wertet es beim Lesen nicht aus.
3. Schlüssel ableiten und Datenschlüssel öffnen
- Passwort nach Unicode NFC normalisieren und als UTF-8 kodieren. So ergibt dasselbe Passwort auf jedem Gerät und mit jeder Tastatur denselben Schlüssel, auch mit Umlauten, Akzenten oder japanischer Eingabe.
KEK = crypto_pwhash(32, passwort, salt, opsLimit, memLimitBytes, ALG_ARGON2ID13)- Zusätzliche Daten der Schlüsselhülle:
AAD_KEY = "FairAuth backup key v1" (ASCII) ‖ salt ‖ u64(opsLimit) ‖ u64(memLimitBytes) Datenschlüssel = crypto_aead_xchacha20poly1305_ietf_decrypt(ciphertext, AAD_KEY, nonce, KEK)
Schlägt Schritt 4 fehl, ist das Passwort falsch oder der Kopf verändert. Weil die KDF-Parameter in AAD_KEY stecken, lassen sie sich nicht unbemerkt ändern.
Der Datenschlüssel bleibt über mehrere Sicherungen gleich, damit FairAuth automatisch sichern kann, ohne jedes Mal nach dem Passwort zu fragen. Beim Passwortwechsel erzeugt FairAuth einen neuen Datenschlüssel.
4. Inhalt entschlüsseln
Direkt nach dem Kopf folgen Rahmen bis zum Dateiende:
| Größe | Inhalt |
|---|---|
| 4 | Länge n des Rahmens (u32), 17 bis 8.388.625 |
n | secretstream-Chiffrat einer Nachricht |
n zählt nur das Chiffrat, nicht die 4 Byte der Längenangabe. Das Chiffrat ist 17 Byte länger als die entschlüsselte Nachricht (crypto_secretstream_xchacha20poly1305_ABYTES: 1 Byte für das verschlüsselte Tag und 16 Byte Authentifizierungscode). Eine entschlüsselte Nachricht hat also höchstens 8 MiB (8.388.608 Byte), ein Rahmen höchstens 8 MiB + 17 Byte (8.388.625 Byte).
Alle Rahmen werden mit demselben Zustand aus crypto_secretstream_xchacha20poly1305_init_pull(payload.header, Datenschlüssel) geöffnet. Zusätzliche Daten jeder Nachricht:
AD = SHA-256(Vorspann ‖ Kopf)
Gehasht werden die ersten 16 + Kopflänge Byte der Datei (Kopflänge ist der Wert an Offset 12), also Byte 0 bis einschließlich Byte 15 + Kopflänge: Vorspann und Kopf, genau so, wie sie in der Datei stehen. Im Beispiel in Abschnitt 9 ist das data[:head_end]. AD sind die 32 Byte des Hashwerts selbst, nicht seine Hex-Darstellung. So ist der Kopf mit dem Inhalt verbunden. Die letzte Nachricht trägt das Tag TAG_FINAL (0x03), alle anderen TAG_MESSAGE (0x00). Mit dem Rahmen, der TAG_FINAL trägt, muss die Datei enden: Folgen danach noch Bytes, wird die Datei abgelehnt. Fehlt TAG_FINAL, ist die Datei unvollständig und wird ebenfalls abgelehnt. secretstream erkennt außerdem vertauschte, fehlende oder doppelte Rahmen.
5. Nachrichten
Jede Nachricht ist ein JSON-Objekt in UTF-8 mit dem Feld type. Die erste Nachricht ist immer manifest, die letzte immer end. Dazwischen stehen tag, smartFolder, image und entry in beliebiger Reihenfolge. Leser überspringen unbekannte Typen und unbekannte Felder.
5.1 manifest
{"type": "manifest", "schema": 1, "app": "FairAuth", "appVersion": "1.0 (1)",
"entries": 42, "images": 3, "tags": 5, "smartFolders": 1}
5.2 entry
{
"type": "entry",
"id": "5B0C2E4A-2B7B-4C4F-9E37-2E6F4C1D8A10",
"issuer": "Example",
"account": "anna@example.com",
"otp": {"kind": "totp", "secret": "JBSWY3DPEHPK3PXP",
"algorithm": "SHA1", "digits": 6, "period": 30},
"otpauth": "otpauth://totp/Example:anna%40example.com?secret=JBSWY3DPEHPK3PXP&issuer=Example&algorithm=SHA1&digits=6&period=30",
"icon": {"kind": "monogram", "monogram": "EX", "accentColor": 3},
"tags": ["0E3F…"],
"favorite": false,
"pinned": false,
"note": "",
"domains": ["example.com"],
"backupCodesSaved": true,
"archived": false,
"migration": "none",
"sortKey": "a0",
"created": "2026-09-20T08:00:00Z",
"modified": "2026-09-25T10:00:00Z",
"trashed": null
}
| Feld | Werte |
|---|---|
otp.kind | totp, hotp, steam |
otp.secret | Base32 nach RFC 4648, Großbuchstaben, ohne = |
otp.algorithm | SHA1, SHA256, SHA512 |
otp.digits | 6 bis 8, bei steam 5 |
otp.period | Sekunden, 10 bis 120 (totp, steam) |
otp.counter | nur hotp, nächster zu verwendender Zähler |
otpauth | derselbe Eintrag als otpauth://-Link, zur einfachen Übernahme in andere Apps. Bei Abweichungen gilt otp |
icon.kind | monogram, image (dann imageID), symbol (dann symbol, ein SF-Symbol-Name) |
icon.accentColor | Ganzzahl 0 bis 11, Index in die zwölf Akzentfarben von FairAuth. FairAuth schreibt das Feld bei jeder Art von icon. Beim Lesen begrenzt FairAuth den Wert auf 0 bis 255 und rechnet für die Anzeige modulo 12; fehlt er, gilt 0 |
migration | none, pending (noch nicht umgezogen), done |
trashed | Zeitpunkt, zu dem der Eintrag in den Papierkorb kam, sonst null |
Steam-Einträge erzeugen fünf Zeichen aus dem Alphabet 23456789BCDFGHJKMNPQRTVWXY; ihr otpauth-Link trägt issuer=Steam und zusätzlich encoder=steam.
5.3 image
{"type": "image", "id": "…", "format": "png", "width": 512, "height": 512,
"shape": "circle", "data": "<Base64>"}
format ist png, heic oder jpeg, höchstens 512 × 512 Pixel, ohne Metadaten (kein EXIF, keine Ortsangaben).
shape ist circle (rund) oder roundedSquare (Quadrat mit abgerundeten Ecken). Jeden anderen Wert und ein fehlendes Feld liest FairAuth als circle.
5.4 tag und smartFolder
{"type": "tag", "id": "…", "name": "Arbeit", "color": 3, "sortKey": "a0"}
{"type": "smartFolder", "id": "…", "name": "Ohne Backup-Codes", "symbol": "tray",
"sortKey": "a1", "filter": {"backupCodesMissing": true, "tagIDs": [], "tagMode": "any"}}
color ist wie icon.accentColor eine Ganzzahl von 0 bis 11 (Index in die zwölf Akzentfarben) und wird beim Lesen genauso behandelt. FairAuth leitet die Farbe eines Tags aus seinem Namen ab und bietet keine Auswahl an; beim Wiederherstellen übernimmt es deshalb nur den Namen und bestimmt die Farbe neu, das Ergebnis ist dieselbe Farbe. Andere Programme dürfen color verwenden.
filter hat die folgenden zehn Schlüssel; FairAuth schreibt immer alle (das Beispiel oben ist gekürzt). Ein Eintrag gehört zum Smart Folder, wenn er alle Bedingungen erfüllt. Fehlt ein Schlüssel oder hat er einen anderen als die genannten Werte, gilt beim Lesen die Vorgabe. Fehlt filter ganz, gelten alle Vorgaben.
| Schlüssel | Werte | Vorgabe |
|---|---|---|
tagIDs | Liste von Tag-IDs (id einer tag-Nachricht). Leer heißt: keine Bedingung | [] |
tagMode | any (mindestens eines der Tags), all (alle Tags) | any |
favoritesOnly | true (nur Favoriten), false | false |
kinds | Liste aus totp, hotp, steam. Leer heißt: alle Arten | [] |
algorithms | Liste aus SHA1, SHA256, SHA512. Leer heißt: alle Algorithmen | [] |
image | any (alle), with (nur mit eigenem Bild), without (nur ohne eigenes Bild) | any |
archived | hide (archivierte ausblenden), only (nur archivierte), include (archivierte einschließen) | hide |
migrationPending | true (nur Einträge mit migration = pending), false | false |
backupCodesMissing | true (nur Einträge mit backupCodesSaved = false), false | false |
query | Text. FairAuth liest höchstens 256 Byte UTF-8 und übernimmt ihn, filtert aber nicht danach | "" |
Ungültige IDs in tagIDs und unbekannte Werte in kinds und algorithms überspringt FairAuth beim Lesen.
5.5 end
{"type": "end", "entries": 42, "images": 3}
FairAuth prüft beim Lesen genau zwei Zahlen, beide aus end: entries muss gleich der Zahl der gelesenen entry-Nachrichten sein, images gleich der Zahl der gelesenen image-Nachrichten. Fehlt eines der beiden Felder oder stimmt eine Zahl nicht, wird die Datei abgelehnt. tag- und smartFolder-Nachrichten zählt end nicht.
Die Zahlen im manifest (Abschnitt 5.1) sind nur eine Vorabangabe: FairAuth prüft sie beim Lesen nicht und liest aus dem manifest nur appVersion. In Dateien von FairAuth entsprechen entries, tags und smartFolders der Zahl der geschriebenen Nachrichten. images zählt dagegen die verschiedenen Bilder, auf die Einträge verweisen. Kann FairAuth eines davon beim Sichern nicht laden, fehlt seine image-Nachricht, und end.images ist kleiner als manifest.images.
6. Grenzen für Leser
| Größe | Grenze |
|---|---|
| Kopf | 64 KiB |
| Nachricht, entschlüsselt | 8 MiB (8.388.608 Byte) |
Rahmen (n in Abschnitt 4) | 8 MiB + 17 Byte (8.388.625 Byte) |
| Nachrichten | 50.000 |
| Einträge | 20.000 |
| Datei | 512 MiB |
FairAuth zählt beim Lesen die Rahmen samt ihrer Längenangaben gegen die Dateigrenze und bricht beim Überschreiten ab. Beim Schreiben prüft es dieselben Grenzen vorher und bricht ab, bevor es eine Datei erzeugt, die ein Leser ablehnen würde: eine Sicherung, die erst beim Wiederherstellen scheitert, fällt zu spät auf. Namen, Konten, Notizen, Tags und Smart-Ordner kürzt FairAuth beim Lesen auf die Grenzen des Tresors, in Byte UTF-8 und an einer Zeichengrenze (VAULT_FORMAT.md, Abschnitt 3).
7. Klartext-Export
Nach erneuter Anmeldung, zwei Bestätigungen und einem deutlichen Hinweis exportiert FairAuth auch unverschlüsselt:
- otpauth-Liste (
.txt): einotpauth://-Link je Zeile. Das verstehen fast alle Authenticator-Apps. - JSON (
.json):{"format": "fairauth-plain", "version": 1, "items": [ … ]}mit denselben Nachrichten wie in Abschnitt 5, ohnemanifestundend.
Eine unverschlüsselte Datei kann jeder lesen, der sie in die Hände bekommt.
8. Dateinamen und Versionen
FairAuth schreibt FairAuth-Backup-JJJJ-MM-TTTHH-MM-SSZ.fairauth (UTC) in den gewählten Ordner, zuerst als temporäre Datei, dann umbenannt. Es behält die fünf neuesten Dateien mit diesem Namensschema und entfernt ältere. Andere Dateien im Ordner fasst es nicht an.
9. Beispiel: Sicherung ohne FairAuth öffnen (Python)
Voraussetzung: Python 3 und PyNaCl 1.5 oder neuer (pip install pynacl).
import base64, hashlib, json, struct, sys, unicodedata
from nacl import bindings, pwhash
def open_backup(path: str, password: str) -> list:
data = open(path, "rb").read()
if data[:8] != b"FAIRAUTH" or data[8] != 1:
raise ValueError("keine FairAuth-Sicherung der Version 1")
head_len = struct.unpack_from("<I", data, 12)[0]
head_end = 16 + head_len
head = json.loads(data[16:head_end])
kdf, wrap = head["kdf"], head["keyWrap"]
salt = base64.b64decode(kdf["salt"])
secret = unicodedata.normalize("NFC", password).encode("utf-8")
kek = pwhash.argon2id.kdf(32, secret, salt,
opslimit=kdf["opsLimit"], memlimit=kdf["memLimitBytes"])
aad = b"FairAuth backup key v1" + salt + struct.pack("<QQ", kdf["opsLimit"], kdf["memLimitBytes"])
data_key = bindings.crypto_aead_xchacha20poly1305_ietf_decrypt(
base64.b64decode(wrap["ciphertext"]), aad, base64.b64decode(wrap["nonce"]), kek)
ad = hashlib.sha256(data[:head_end]).digest()
state = bindings.crypto_secretstream_xchacha20poly1305_state()
bindings.crypto_secretstream_xchacha20poly1305_init_pull(
state, base64.b64decode(head["payload"]["header"]), data_key)
items, pos, final = [], head_end, False
while pos < len(data):
size = struct.unpack_from("<I", data, pos)[0]
pos += 4
message, tag = bindings.crypto_secretstream_xchacha20poly1305_pull(
state, data[pos:pos + size], ad)
pos += size
items.append(json.loads(message))
if tag == bindings.crypto_secretstream_xchacha20poly1305_TAG_FINAL:
final = True
break
if not final or pos != len(data):
raise ValueError("Datei unvollständig oder verändert")
return items
if __name__ == "__main__":
for item in open_backup(sys.argv[1], sys.argv[2]):
if item["type"] == "entry":
print(item["otpauth"])
Das Programm gibt jeden Eintrag als otpauth://-Link aus. Diese Links lassen sich in nahezu jede Authenticator-App übernehmen.
Zwei Testvektoren liegen bei: backup-testvector-de.fairauth (Passwort mit Umlauten) und backup-testvector-ja.fairauth (japanisches Passwort). Passwörter und erwartete Ausgabe stehen in backup-testvector.txt. Beide Dateien sind mit diesem Programm und zusätzlich mit der Argon2-Referenzimplementierung geprüft.