Repository navigation
Long secret setting values are silently dropped and never reach the player, but setting set reports success #330
Description
Summary
Setting a long value on a type: secret Edge App setting reports success, both through screenly edge-app setting set and in the dashboard, but the value never reaches the player. Short values on the same setting are delivered within seconds. Nothing tells the user the value was dropped, so the app just behaves as if the secret was never set.
This came up with a Replit external access token (a JWT, 315 characters), which we need to send as a bearer token.
Steps to reproduce
- Declare a secret setting in
screenly.yml:settings: bearer_token: type: secret title: Bearer Token optional: true help_text: properties: help_text: ... type: string schema_version: 1
- Deploy the app and create an instance on a screen.
- Set a long value (315 characters):
Setting the same value in the dashboard also appears to save.
screenly edge-app setting set 'bearer_token=<315-character JWT>' # Edge App setting successfully set.
- Watch the player.
Expected
Either the secret reaches the player, or the CLI and dashboard reject the value with a clear error (for example "secret values are limited to 190 bytes").
Actual
- The CLI prints
Edge App setting successfully set.and the dashboard saves without error. - The player's
playlist.jsoncontains no entry for the instance undercipher_data.secrets. Other apps on the same screen have theirs. - The player logs this on every load:
App edge-app://<app_id>/<instance_id>/stable/8/<hash> has no secrets. screenly.settings.bearer_tokenis missing inside the app.
Setting a short value right after (short-test-value-123) worked straight away: the instance appeared under cipher_data.secrets in the next sync, about 8 seconds later.
Setting the long value again afterwards confirmed it: the CLI reported success and the player got a playlist.json update about 5 seconds later, so the change registered, but the instance had been removed from cipher_data.secrets. The server accepts the value and then drops it when encrypting it for the player.
Measured limit
Testing with values of different lengths on the same setting, each checked against the player's playlist.json after the update arrived:
| Length (ASCII chars) | Delivered to player |
|---|---|
| 20 | ✅ |
| 190 | ✅ (tested twice) |
| 191 | ❌ |
| 193, 196, 202, 214, 315 | ❌ |
The maximum is exactly 190 bytes, which matches RSA-2048 with OAEP-SHA256 padding: 256 − 2×32 − 2 = 190.
This rules out most JWTs. ES256-signed tokens are typically 250–400 characters and RS256-signed ones 500+, so bearer tokens for most services can't be stored as secrets today.
Typical token lengths
190 bytes is shorter than most real-world bearer tokens, and the person setting up the screen usually can't make them shorter.
A JWT is header.payload.signature, base64url-encoded. The header is about 36 characters and the payload varies. The signature depends on the algorithm:
| Signing algorithm | Signature | Typical full token | Examples |
|---|---|---|---|
| HS256 | 43 | ~150–300 | Self-issued tokens |
| ES256 | 86 | ~250–400 | Replit external access tokens (ours: 315) |
| ES384 | 128 | ~300–450 | |
| RS256 | 342 | ~500–1,200+ | Auth0, Google, Azure AD / Entra ID |
For Replit in particular, we checked whether a shorter credential is available. External access tokens are the only token type for private deployments, and none of the options change their size: the expiry only changes the 10-digit exp claim, the label isn't part of the token, and Development and Production tokens have the same format. Replit's other access mode, Password protected, uses a shared password that would fit in 190 bytes. It can be disabled by workspace admins, and its docs don't say how the password is checked, so it isn't a general alternative either.
So any Edge App that authenticates to a typical OAuth or JWT-based service can't keep its token in a type: secret setting.
Likely cause
Encrypted secret values in playlist.json are 344 characters of base64, which is 256 bytes and suggests RSA-2048 encryption per value. With OAEP padding, RSA-2048 can encrypt at most 190 bytes with SHA-256, which is the measured limit above. Longer values seem to be dropped silently on the server instead of being rejected.
Suggestions
- Support longer secrets. Encrypt each value with a random symmetric key (for example AES-GCM) and wrap that key with the player's RSA key, so values of any length can be sent. This isn't a CLI change: setting the value in the dashboard fails the same way, and the value is dropped when the server encrypts secrets for the device. It needs:
- API: encrypt with the hybrid scheme when building
cipher_data.secretsfor a screen. - Player client (
screenly-client): decrypt the new format. It currently decrypts each value with RSA directly ("Decrypted N of N secrets through NSS"). Players that haven't been updated would still need the current format for short values.
- API: encrypt with the hybrid scheme when building
- Until then, return an error from the API when a secret is too long, and show it in the CLI (
setting set) and the dashboard instead of reporting success. - Document the maximum secret length (currently 190 bytes).
Environment
- Screenly CLI 26.8.0 (macOS)
- Player: Raspberry Pi 4, Ubuntu Core 24,
screenly-clientsnap 26.10.0-pi64 - Edge App built with
@screenly/edge-apps26.8.0
Activity