Encryption

What is encrypted at DeployYourApp and what is not — TLS in transit, field-level encryption at rest, and why update bundles are deliberately not encrypted.


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 publicKey configured, or no signature supplied, the update is refused with SIGNATURE_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-https bundle 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 updateUrl and statsUrl hosts, plus anything in allowedDownloadHosts.
  • 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_SECRET by 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 the CLOUD_BUILDS_ENABLED feature 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.


Previous
Code Signing
Next
Authentication