Bundle Encryption

The DYA1 wire format, AES-256-GCM parameters, RSA-OAEP session key wrapping, and how bundles are protected in transit and at rest.


Bundle encryption makes your update payload unreadable to everyone except your own app, including to DeployYourApp. This page covers the exact format and parameters. Signing is covered separately in Code signing.

Turning it on

Encryption activates when dya-encryption-public.pem is present in the directory you run dya deploy from. There is no flag. Remove the file to deploy in the clear.

dya keys generate creates the encryption pair alongside the signing pair. The private half, dya-encryption-private.pem, is the decryption key and must be compiled into your app's native binary — never placed in web assets and never uploaded.

Capacitorcapacitor.config.json:

{
  "plugins": {
    "DeployYourApp": {
      "appId": "com.example.app",
      "encryptionPrivateKey": "-----BEGIN PRIVATE KEY-----\nMIIJQgIBADAN...\n-----END PRIVATE KEY-----"
    }
  }
}

Electron — pass it to the constructor:

import { ElectronUpdateManager } from '@deploy-your-app/electron-update-manager';

const updater = new ElectronUpdateManager({
  appId: 'com.example.app',
  encryptionPrivateKey: process.env.DYA_ENCRYPTION_PRIVATE_KEY,
});

If the server returns a wrapped session key and no encryptionPrivateKey is configured, the update fails. The Electron updater throws Encryption private key required for E2E decryption. Set config.encryptionPrivateKey.

Parameters

Property Value
Cipher AES-256-GCM
Key length 256 bits (32 bytes), randomly generated per bundle
Nonce 96 bits (12 bytes), randomly generated per bundle
Authentication tag 128 bits (16 bytes)
Additional authenticated data None
Key wrapping RSA-OAEP, SHA-256 digest, MGF1-SHA-256, empty label
Wrapping key RSA-4096 encryption public key
Wrapped key encoding Base64

The AES key is generated fresh for every deploy. It is never written to disk and never sent to the server unwrapped.

Wire format

The encrypted bundle is a single binary blob with a versioned header. Version 1 is identified by the four-byte ASCII magic DYA1.

Offset Length Contents
0 4 ASCII magic DYA1
4 12 AES-GCM nonce
16 n AES-256-GCM ciphertext
end − 16 16 GCM authentication tag

The plaintext inside the ciphertext is the compressed bundle ZIP.

A payload that does not start with DYA1, or that is not longer than 32 bytes, is rejected by all three clients with the message Unrecognized encrypted bundle format. Re-deploy the bundle with the current CLI.

The wrapped session key travels separately

The RSA-wrapped AES key is not part of the blob. The CLI puts it in the bundle's manifest at upload time, and the server returns it to the device as the sessionKey field of the update-check response:

{
  "available": true,
  "version": "1.4.0",
  "url": "https://...",
  "checksum": "3f1a...",
  "signature": "MEUCIQ...",
  "sessionKey": "Zk9wS2..."
}

sessionKey is null for bundles that were deployed unencrypted. A client that receives null treats the download as plaintext.

Deploy-time flow

Compressed bundle ZIP
       |
       v
Generate random 256-bit AES key + 96-bit nonce
       |
       v
AES-256-GCM encrypt  ->  DYA1 || nonce || ciphertext || tag
       |
       v
Wrap the AES key with RSA-OAEP (SHA-256 / MGF1-SHA-256)  ->  base64 sessionKey
       |
       v
SHA-256 checksum over the encrypted bytes
       |
       v
RSA-SHA256 signature over the same encrypted bytes
       |
       v
Upload blob + checksum + signature + manifest.sessionKey

Encrypting before checksumming and signing is what makes server-side verification possible: the checksum and signature cover the exact bytes the server receives and stores.

Device-side flow

Every client does the same four steps, in this order:

  1. Verify the SHA-256 checksum of the downloaded bytes.
  2. Verify the RSA signature over the same bytes, if a publicKey is configured. See Code signing.
  3. Unwrap the AES session key with the embedded encryption private key, using RSA-OAEP with SHA-256. All three clients assert or require a 32-byte result.
  4. Parse the DYA1 header, then run AES-GCM decryption over nonce, ciphertext, and tag.

Verification happens on the ciphertext, before decryption. That means a tampered download is rejected without the private key ever being applied to attacker-controlled bytes.

GCM is authenticated encryption. If a single ciphertext or tag byte changes, decryption fails outright — independently of the RSA signature. The two checks defend against different things: GCM proves the bytes decrypt to what was encrypted under that key; the signature proves the bytes came from your signing key.

The platform APIs differ; the parameters do not.

Client Unwrap Decrypt
iOS SecKeyCreateDecryptedData with .rsaEncryptionOAEPSHA256 CryptoKit AES.GCM.open over a SealedBox(combined:)
Android Cipher.getInstance("RSA/ECB/OAEPPadding") with OAEPParameterSpec("SHA-256", "MGF1", MGF1ParameterSpec.SHA256, PSource.PSpecified.DEFAULT) Cipher.getInstance("AES/GCM/NoPadding") with GCMParameterSpec(128, nonce)
Electron privateDecrypt with RSA_PKCS1_OAEP_PADDING and oaepHash: 'sha256' createDecipheriv('aes-256-gcm', key, iv) with an explicit auth tag

Android names MGF1-SHA-256 explicitly rather than relying on the OAEPWithSHA-256AndMGF1Padding transformation string, because some Android providers default MGF1 to SHA-1 and would then fail to unwrap keys produced by the CLI.

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.

Encryption at rest

Bundle objects are stored in MinIO exactly as uploaded. DeployYourApp does not apply a second layer of encryption to them.

  • With bundle encryption on, what is stored is the DYA1 ciphertext. The unwrapping key exists only inside your app binary, so we cannot read it.
  • With bundle encryption off, what is stored is a plain ZIP that our operators can read.

Whether the underlying volume is encrypted is a property of the deployment's storage, not of this application.

One database field is encrypted at the application layer: cloud-build environment variables are stored with AES-256-GCM 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. See Authentication.

Operational notes

  • Losing dya-encryption-private.pem means no device can decrypt bundles encrypted under its public half. Back it up wherever you back up your signing material.
  • Rotating the encryption pair requires a native release, because the private key is compiled into the binary. Devices on the old binary cannot decrypt bundles wrapped under the new key.
  • In CI, restore dya-encryption-public.pem into the working directory the same way you restore the signing key:
- run: |
    echo "$ENCRYPTION_PUBLIC_KEY" > ./dya-encryption-public.pem
    echo "$SIGNING_KEY" > ./dya-signing-private.pem
    dya login --api-key "$DYA_API_KEY"
    dya deploy
  env:
    ENCRYPTION_PUBLIC_KEY: ${{ secrets.DYA_ENCRYPTION_PUBLIC_KEY }}
    SIGNING_KEY: ${{ secrets.DYA_SIGNING_KEY }}
    DYA_API_KEY: ${{ secrets.DYA_API_KEY }}

Previous
Code Signing
Next
Authentication