The app is designed so plaintext submissions do not need to pass through the Worker. The browser encrypts responses locally, then the server stores the encrypted payload and related metadata.
- Client-side encryption: submissions are encrypted before upload with the public key embedded in the rendered form.
- Role-based access: only creators and admins can manage forms, and creator access is granted through the request workflow.
- One-time download tokens: generated exports are served through single-use tokens in KV so archives cannot be reused indefinitely.
- Local decryption: helper scripts and native runtime snippets exist for offline inspection of secrets and exported submissions.
Secrets and keys
The form secret controls edits to an existing form definition. The private key stays local and is only used when decrypting secret payloads or exported submission archives.
const crypto = require('crypto');
function decryptPayload(encryptedBase64, privateKeyPem) {
const jsonString = Buffer.from(encryptedBase64, 'base64').toString('utf-8');
const envelope = JSON.parse(jsonString);
const { encryptedKey, iv, encryptedData } = envelope;
const aesKeyBuffer = crypto.privateDecrypt(
{
key: privateKeyPem,
padding: crypto.constants.RSA_PKCS1_OAEP_PADDING,
oaepHash: 'sha256'
},
Buffer.from(encryptedKey, 'base64')
);
const encryptedDataBuffer = Buffer.from(encryptedData, 'base64');
const tagLength = 16;
const ciphertext = encryptedDataBuffer.subarray(0, encryptedDataBuffer.length - tagLength);
const tag = encryptedDataBuffer.subarray(encryptedDataBuffer.length - tagLength);
const decipher = crypto.createDecipheriv('aes-256-gcm', aesKeyBuffer, Buffer.from(iv, 'base64'));
decipher.setAuthTag(tag);
const decrypted = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
return decrypted.toString('utf-8');
}<?php
function decryptPayload($encryptedBase64, $privateKeyPem) {
$jsonString = base64_decode($encryptedBase64);
$envelope = json_decode($jsonString, true);
$encryptedKey = $envelope['encryptedKey'];
$iv = $envelope['iv'];
$encryptedData = $envelope['encryptedData'];
openssl_private_decrypt(
base64_decode($encryptedKey),
$aesKey,
$privateKeyPem,
OPENSSL_PKCS1_OAEP_PADDING
);
$encryptedDataBuffer = base64_decode($encryptedData);
$tag = substr($encryptedDataBuffer, -16);
$ciphertext = substr($encryptedDataBuffer, 0, -16);
return openssl_decrypt(
$ciphertext,
'aes-256-gcm',
$aesKey,
OPENSSL_RAW_DATA,
base64_decode($iv),
$tag
);
}import javax.crypto.Cipher;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.OAEPParameterSpec;
import javax.crypto.spec.PSource;
import javax.crypto.SecretKey;
import java.security.KeyFactory;
import java.security.PrivateKey;
import java.security.spec.MGF1ParameterSpec;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.Base64;
public class DecryptPayload {
public static String decryptPayload(String encryptedBase64, String privateKeyPem) throws Exception {
// Parse the envelope, unwrap the AES key, then decrypt the AES-GCM payload.
return "";
}
}import Foundation
import Security
func decryptPayload(encryptedBase64: String, privateKeyPem: String) throws -> String {
// Parse the envelope, unwrap the AES key with RSA-OAEP, then decrypt AES-GCM content.
return ""
}import 'dart:convert';
import 'package:cryptography/cryptography.dart';
Future<String> decryptPayload(String encryptedBase64, String privateKeyPem) async {
// Parse the envelope, unwrap the AES key, then decrypt the payload.
return '';
}import crypto from 'react-native-quick-crypto';
export function decryptPayload(encryptedBase64: string, privateKeyPem: string): string {
// Parse the envelope, unwrap the AES key, then decrypt the payload.
return '';
}