Three different things get called "encryption" around an OTA platform, and only two of them exist here. This page says which is which.
| Layer | Status |
|---|---|
| Transport (API calls, bundle downloads) | Encrypted with TLS. See Encryption in transit. |
| Sensitive database fields | Encrypted at rest with AES-256-GCM. See Encryption at rest. |
| Bundle payloads | Not encrypted, deliberately. See below. |
Bundles are not encrypted
Earlier versions of the CLI and the clients supported end-to-end encrypted bundles: the archive
was sealed with AES-256-GCM under a session key wrapped to a key compiled into your app. That
feature has been removed. There is no encryptionPrivateKey configuration option, no
sessionKey field in the update-check response, and no decryption code in the Capacitor plugin,
the Electron updater, the CLI, or the server.
It was removed because it was the direct cause of an App Store rejection under Guideline 2.5.2. An encrypted payload is opaque to App Review — it asks a reviewer to accept, on your word, that a blob they cannot open contains nothing but web assets. That is precisely the shape of the thing the guideline exists to catch.
What you give up is confidentiality of the bundle against someone watching your CDN. What you gain is that the payload is inspectable: a reviewer handed a bundle URL can download the ZIP and read every file in it, and so can you. Signing already proved a bundle was yours; encryption was never what made it trustworthy.
Two things are worth knowing about what you are and are not losing:
- The removed scheme was real confidentiality — the app held the RSA private key. Some other
OTA plugins advertise "end-to-end encryption" while embedding an RSA public key in the app
and recovering the session key with
c^e mod n. That construction proves origin; it does not keep anything secret, because the key that opens the bundle ships in every copy of the app. - Your bundle is still in transit encrypted (TLS, below). What changed is that we, and anyone with read access to the object store, can read the archive at rest.
What protects a bundle instead
Every item below is enforced in code, not by configuration:
- Mandatory RSA-4096 signature verification. Each client verifies the signature over the
downloaded bytes before the archive is opened. With no
publicKeyconfigured, or no signature supplied, the update is refused withSIGNATURE_REQUIRED. There is no fail-open path a server can trigger by omitting a field. See Code signing. - SHA-256 checksum over the same bytes, verified first.
- A per-file SHA-256 manifest shipped inside every bundle at
.dya-manifest.json, checked in both directions during extraction: every declared file must be present with the declared hash, and every extracted file must be declared. A bundle with no readable manifest is refused. - A web-assets-only policy. Archive entries must carry an extension from a fixed allowlist, and every extracted file's leading bytes are checked against a table of native executable formats regardless of its name. The updater is structurally incapable of installing native code.
- Archive hardening. Path-traversal entries are rejected, no symlink is ever created, and per-entry and total size caps bound decompression.
- A signing private key that never leaves your machine, so a compromised CDN or storage bucket still cannot produce a bundle your app will accept.
The normative list of allowed extensions and executable magic numbers lives in
packages/shared/src/constants/bundlePolicy.js, mirrored in the Swift and Kotlin clients. See
App Store review for the full argument, and the CLI's
bundle asset policy for what fails at deploy time.
A leftover key pair
dya keys generate still writes dya-encryption-public.pem and dya-encryption-private.pem
alongside the signing pair, and still uploads the public half. Nothing reads them: dya deploy
does not encrypt, and no client decrypts. Do not embed the private half in your app, and do
not wire it into a plugin or updater config expecting it to decrypt anything. See
CLI key commands.
Encryption in transit
All API and download traffic is HTTPS. The server sets HSTS with max-age=31536000,
includeSubDomains, and preload.
Bundle downloads use a presigned object-storage URL valid for 1 hour, or a direct CDN URL
when BUNDLE_CDN_URL is configured. Presigned URLs are minted per update check and are not
listed anywhere.
The clients pin the transport independently of the server's response:
- A non-
httpsbundle URL is refused outright. Plain HTTP is tolerated only against loopback, for a local MinIO in development. - The download host must be one the app is configured to talk to — the
updateUrlandstatsUrlhosts, plus anything inallowedDownloadHosts. - Every redirect hop is re-validated against the same scheme and host rules, and a rejected hop cancels the download. An open redirect on an allowed host cannot bounce the download somewhere else.
Encryption at rest
Bundle objects are stored in MinIO exactly as uploaded, as plain ZIP archives that our operators can read. DeployYourApp does not apply a layer of encryption to them. Whether the underlying volume is encrypted is a property of the deployment's storage, not of this application.
Sensitive database fields are encrypted at the application layer with AES-256-GCM:
- Two-factor secrets and backup codes are stored symmetrically encrypted under
BETTER_AUTH_SECRETby Better Auth. - Cloud-build environment variables are stored under the server's
FIELD_ENCRYPTION_KEY(a 32-byte key supplied as 64 hex characters). Cloud builds are currently disabled by theCLOUD_BUILDS_ENABLEDfeature flag.
Passwords and API keys are not encrypted — they are hashed, which is the stronger property for a credential you never need to read back. See Authentication.
Related
- Code signing — the signature that replaces encryption as the trust anchor
- App Store review — why an inspectable bundle is the better trade
- Security overview
- Authentication and access control