OpenCode AI e9899d2e6d
Some checks failed
Android CI Build / check (push) Has been cancelled
Android CI Build / package-apks (push) Has been cancelled
Docs Deploy / build (push) Has been cancelled
Module Market Check / Validate modules/ (push) Has been cancelled
Module Market Publish / Generate submissions.json (push) Has been cancelled
Docs Deploy / deploy (push) Has been cancelled
Initial DruksCode V2: fresh fork of web-to-app (f3b488e) with DruksCode identity
- Package rename com.webtoapp -> br.com.drukstech.codeapp
- Product rename WebToApp/web-to-app -> DruksCode/drukscode
- Design system Wta* -> Dkc*
- applicationId br.com.drukstech.codeapp (.play for gplay flavor)
- rootProject.name DruksCode
- Fresh git history (upstream kept as remote for sync)
2026-09-12 02:42:02 +02:00

137 lines
4.9 KiB
Markdown

# Remote activation — reference verification server
A Cloudflare Worker that speaks the contract described in
[`.github/docs/remote-activation.md`](../../.github/docs/remote-activation.md).
It is the "I don't want to build a backend" answer to remote activation: copy,
paste, deploy.
What you get:
- Codes held in Workers KV, revocable and rotatable without rebuilding an APK
- Every response signed with **EC P-256 / SHA-256**, so a fake server or a
man-in-the-middle cannot forge an `ok: true`
- Nonce echo (replay protection) and device binding for one-seat codes
- Optional dynamic URL delivery, with or without AES-256-GCM encryption
## 1. Generate a signing key
The app verifies responses with the public half; the Worker signs with the
private half.
```bash
# private key, PKCS#8, single-line Base64 — this is the Worker's SIGNING_KEY
openssl ecparam -genkey -name prime256v1 -noout -out ec.pem
openssl pkcs8 -topk8 -nocrypt -in ec.pem -out ec-pkcs8.pem
openssl base64 -A -in ec-pkcs8.pem -out ec-pkcs8.b64
# public key, SPKI — this goes into the app's "Signature public key" field
openssl ec -in ec.pem -pubout -out ec-pub.pem
openssl base64 -A -in ec-pub.pem -out ec-pub.b64
```
Keep `ec.pem` and `ec-pkcs8.b64` out of git. The public key is not a secret.
## 2. Deploy
```bash
cd examples/remote-activation-worker
npm install
npx wrangler login
npm run kv:create # paste the printed id into wrangler.toml
npx wrangler secret put SIGNING_KEY < ec-pkcs8.b64
npx wrangler deploy
```
Then open the deployed URL in a browser. A healthy worker answers:
```json
{
"ok": true,
"publicKey": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE…",
"keyFingerprint": "3f9a1c02…",
"urlEncryption": "disabled",
"problems": []
}
```
`publicKey` is the SPKI Base64 of your key — paste it straight into the app's
**Signature public key** field. If `problems` is not empty, the worker will not
verify anything and will say why.
## 3. Add codes
One KV entry per code. The key is `code:` followed by the uppercased code.
```bash
# unlimited activations
npx wrangler kv:key put --binding=ACTIVATION_CODES "code:DEMO1234" \
'{"note":"demo"}'
# expires at a fixed date (epoch millis)
npx wrangler kv:key put --binding=ACTIVATION_CODES "code:YEAR2027" \
'{"expiresAt":1798761600000,"maxDevices":1,"devices":[]}'
# locked to one package, delivering an encrypted target URL
npx wrangler kv:key put --binding=ACTIVATION_CODES "code:VIP0001" \
'{"packageName":"com.example.app","url":"https://example.com/target","encryptUrl":true}'
```
`encryptUrl` requires an AES key on the worker:
```bash
openssl rand -base64 32 | tr -d '\n' | xargs -0 npx wrangler secret put AES_KEY
```
### Code record fields
| Field | Type | Meaning |
| --- | --- | --- |
| `expiresAt` | number \| null | Absolute expiry, epoch millis. Omit for never. |
| `maxDevices` | number | Seats, enforced only when the app sends `deviceBound: true`. Default 1. |
| `devices` | string[] | Device ids that have redeemed this code. Maintained by the worker. |
| `packageName` | string \| null | If set, the code only works for that package. |
| `url` | string \| null | Enables URL delivery. See the caveat below. |
| `encryptUrl` | boolean | AES-256-GCM encrypt `url`. Requires `AES_KEY`. |
| `message` | string | Returned to the client on success. |
| `note` | string | Your own bookkeeping; never leaves the server. |
## Two things to know before you go live
**URL delivery is configured on both sides, and they must agree.** The request
the app sends does not say whether it expects a delivered URL, so the worker
cannot tell. The rule is therefore: a code with a `url` is for an app with
**Deliver target URL** enabled, and a code without one is for an app with it
disabled. Mismatched, the signature check fails on every activation, because the
app and the server disagree about whether the URL is part of the signed payload.
**Workers KV is eventually consistent and has no compare-and-swap.** Two devices
redeeming the last seat of a device-bound code in the same instant can both
succeed. For most apps this is an acceptable risk at this scale; if it is not for
yours, move the record store to D1 or a Durable Object — the signing path is
unchanged, only `get`/`put` differ.
## Verifying your changes
`test/protocol-check.mjs` drives the real worker with a mock KV and verifies each
response the way the Android client does — same payload construction, same
ECDSA P-256 check in P1363 form, same AES-GCM wire format.
```bash
npm run check
```
It is the fastest way to catch a change that would silently break every
installed app.
## Protocol details worth not breaking
Both live in `RemoteActivationVerifier.kt` on the app side, and both are
reproduced in `src/index.js`:
1. The signature field is **`sig`**, not `signature`.
2. The signed payload has a fixed key order — `ok`, `expiresAt`,
`remainingUses`, `nonce`, then `url` when URL delivery is on — with `null`
collapsed to `0` for `expiresAt` and `-1` for `remainingUses`.