taler-docs

Documentation for GNU Taler components, APIs and protocols
Log | Files | Refs | README | LICENSE

commit ee0c431687c3cb8f0d8f51fa9f5acf48d42de680
parent 60d01146b4977933feb2a53257c4f1f75cc626ba
Author: Iván Ávalos <avalos@disroot.org>
Date:   Mon,  3 Aug 2026 14:23:59 +0200

DD92: document the backup process, schedule and wallet-core API

Also corrects the block nonce size and the blob keying to what the wallet
and the sync server actually implement.

Diffstat:
Mdesign-documents/092-incremental-backup-sync.rst | 211+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------
1 file changed, 192 insertions(+), 19 deletions(-)

diff --git a/design-documents/092-incremental-backup-sync.rst b/design-documents/092-incremental-backup-sync.rst @@ -137,22 +137,33 @@ reconciliation mechanism that will be discussed in further sections. Block format ++++++++++++ -Each block will consist of a 2-byte version number, a random 32-byte nonce, +Each block will consist of a 2-byte version number, a random 24-byte nonce, and a gzip-compressed JSON object with its length. The block will be padded up -to the next whole kilobyte for privacy reasons. +to the next whole kilobyte for privacy reasons. A block whose length is +already a multiple of a kilobyte is not padded further. + +The nonce is 24 bytes because that is exactly what `secretbox`_ takes, which +lets a block be encrypted under its own nonce. Encryption will be performed on the block using symmetric authenticated -encryption via libsodium's `secretbox`_ function, with a 32-bit key derived -from the wallet's backup encryption key and the hash of the entire plaintext -block, which in the final implementation should be shareable between any -wallets that the user wishes to add to the synchronization group. +encryption via libsodium's `secretbox`_ function, with a 32-byte key derived +from the wallet's backup encryption key and the nonce of the block, which in +the final implementation should be shareable between any wallets that the +user wishes to add to the synchronization group. + +.. note:: + + The key is derived from the *nonce* rather than from the hash of the + plaintext block: the nonce travels with the block, whereas the plaintext + hash is only known to whoever can already decrypt it, so deriving from it + would make the block undecryptable. .. code-block:: text +----------------------------+ | version number (2 byte) | +----------------------------+ - | nonce (32 byte) | + | nonce (24 byte) | +----------------------------+ | JSON length n (4 byte) | +----------------------------+ @@ -178,6 +189,14 @@ payload follows the common Taler signing structure with a ``purpose`` field (see :ref:`Signatures` in the API common conventions for the general format). The specific payloads are: +.. warning:: + + The three purpose numbers below are **not in the GANA registry yet**, so + both the server and the wallet spell them out locally. Until they are + registered, the wire format depends on whether a given build happened to + have them defined, and a mismatch shows up only as a signature that fails + to verify. + .. sourcecode:: c /** @@ -517,10 +536,27 @@ Blob format Similar to blocks, each blob will consist of 2-byte version number, the 4-byte data length, the gzipped data, and a padding to the next whole kilobyte. The blob will be encrypted using a key derived from the wallet's backup encryption -key and the hash of the unencrypted file. +key and the hash of the unencrypted file: -The hash used to index the object in the store will be computer from the -encrypted blob using SHA-512 and truncated to 32 bytes. +.. code-block:: text + + key = KDF(32, backup_key, "taler-sync-blob-secret-salt", H(plaintext)) + uid = H(key) + +Every blob therefore has its own key. The 64-byte ``uid``, which is the +SHA-512 hash of that key, is what indexes the object in the store and is the +only one of the two the sync server ever learns; the key itself is stored +*inside the blocks* that reference the blob, where it doubles as the +reference to the object that has to be fetched. + +The key is thus all a wallet needs to both locate and decrypt a blob, which +is the only thing a block carries. The `secretbox`_ nonce is consequently +derived from the key as well, as the first 24 bytes of ``H(key)``. Nonce +reuse cannot occur, because distinct plaintexts derive distinct keys. + +Because the key is derived from the plaintext, blobs are content-addressed: +identical contents yield the same key, UID and ciphertext, so an unchanged +blob is only ever uploaded once. .. code-block:: text @@ -1625,13 +1661,68 @@ denomination. In turn, all the sign and spend operations of the deleted coins must also be deleted, since they are in the ``coins`` deletion group and thus contain a reference to a coin. -.. TODO: - Backup process - -------------- +Backup process +-------------- -.. TODO: - Backup schedule - --------------- +Collecting increments +~~~~~~~~~~~~~~~~~~~~~ + +Wallet transactions record what they changed by appending increments to a +pending buffer, held in the wallet's backup configuration record. The +recording happens **within the same database transaction that performs the +change**, so that the change and the increment describing it commit together. +A wallet can therefore never end up in a state that its backup does not know +about, however abruptly it is shut down. + +Increments are recorded as a flat list; which section of the +``IncrementSet`` an increment lands in is derived from its +``type``, so that transaction code never names a section itself and a new +increment type cannot be added without giving it a home. + +A wallet that has not set up backup yet has no encryption key to protect the +increments with, so recording is a no-op rather than an error. + +The backup cycle +~~~~~~~~~~~~~~~~ + +One cycle takes whatever increments have accumulated, packs them into a +block, and appends that block to the account's chain: + +1. In a single database transaction, move the pending increments out of the + buffer and into an *in-flight block*, storing its nonce, hash, contents + and the nonce of the block it is to be appended after. +2. Upload any blobs the block references, then the block itself. +3. Once the provider has acknowledged the block, discard the in-flight block + and advance the pointer to the last acknowledged block. + +The hand-over in step 1 is what makes the cycle resilient: the increments are +never absent from both the buffer and a block. A wallet that dies at any +point either finds increments still pending, or finds an in-flight block and +retries it — under its **original nonce**, which the server answers with +``304 Not modified`` if the upload did in fact land. Increments are thus +neither lost nor backed up twice, and a cycle that has packed a block always +retries it before packing new increments, so the chain stays ordered. + +Backup schedule +--------------- + +A backup runs at *critical points* of wallet operations, and on a schedule +otherwise. + +A critical point is one past which losing the device loses money or user +data that cannot be reconstructed. The canonical example is a withdrawal: +coin secrets are derived from the withdrawal group's seed, so a backup is +triggered once every planchet has been generated and persisted but **before** +the exchange is asked to sign them. Past that point the exchange considers +the coins withdrawn while a wallet restored from an older backup could no +longer reconstruct them. + +Triggering is only a nudge, and deliberately carries no guarantee: it happens +after the recording transaction has committed, and nothing depends on it +having run. Independently, a periodic task runs a cycle at a fixed interval, +which is what catches increments whose trigger never fired — because the +wallet was killed, was offline, or because the operation that produced them +has no critical point of its own. .. TODO: Restore process @@ -1641,14 +1732,96 @@ contain a reference to a coin. Restore schedule ---------------- +Wallet-core API +--------------- + +Backup providers and the wallet's backup key are managed through the +wallet-core API. All requests below are available on every platform. + +.. note:: + + The request handlers are currently declared but not yet implemented; the + collection and scheduling mechanisms described above are. + +.. ts:def:: AddBackupProviderRequest + + interface AddBackupProviderRequest { + backupProviderBaseUrl: string; + + name: string; + + // Activate the provider. Should only be done after + // the user has reviewed the provider. + activate?: boolean; + } + +``addBackupProvider`` registers a sync server. Since a provider may charge an +annual fee, the response either confirms the provider is ready or hands back +a ``taler://pay/...`` URI for the account payment: + +.. ts:def:: AddBackupProviderResponse + + type AddBackupProviderResponse = + | { status: "ok" } + | { status: "payment-required"; talerUri?: string }; + +``removeBackupProvider`` takes a `RemoveBackupProviderRequest` naming the +provider by base URL and returns an empty object. + +.. ts:def:: RemoveBackupProviderRequest + + interface RemoveBackupProviderRequest { + backupProviderBaseUrl: string; + } + +``getBackupInfo`` reports the wallet's backup identity and the state of each +known provider, including its terms, payment status and the outcome of the +last backup attempt. + +.. ts:def:: BackupInfo + + interface BackupInfo { + walletRootPub: string; + deviceId: string; + providers: ProviderInfo[]; + } + +``getBackupRecovery`` returns the secret needed to restore the wallet on +another device, along with the providers to fetch the blocks from. It is +what the user backs up out of band, and what a restoring wallet is fed. + +.. ts:def:: BackupRecovery + + interface BackupRecovery { + walletRootPriv: string; + providers: { + name: string; + url: string; + }[]; + } + +Account keys are not part of any of these payloads: they are derived from the +wallet root key and the provider's base URL, so each provider sees an +unlinkable account public key and only the root key has to be preserved. + +.. code-block:: text + + account_priv = KDF(32, wallet_root_priv, + "taler-sync-account-key-salt", provider_base_url) + Definition of done ================== * [x] Design backup schema. * [ ] Design incremental sync. -* [ ] Design backup/restore schedules. -* [ ] Design wallet-core API. -* [ ] Wallet-core implementation. +* [ ] Design backup/restore schedules (partial: backup process and schedule + done; restore still missing). +* [x] Design wallet-core API. +* [ ] Wallet-core implementation (partial: block and blob encoding, CRDT + merge, sync protocol client and signatures, increment collection and the + scheduled backup cycle done; the API request handlers, blob upload, + fetching and merging remote blocks, item deletion and the remaining + increment types still missing). * [x] Design sync API (+ auth). * [ ] Server-side implementation (partial: block GET/POST/PUT/DELETE, object store GET/POST with reference counting, /config and payments done;