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.
Capacitor — capacitor.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:
- Verify the SHA-256 checksum of the downloaded bytes.
- Verify the RSA signature over the same bytes, if a
publicKeyis configured. See Code signing. - 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.
- Parse the
DYA1header, 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
DYA1ciphertext. 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.pemmeans 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.peminto 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 }}