Skip to content

Long secret setting values are silently dropped and never reach the player, but setting set reports success #330

Open

Description

@salmanfarisvp

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

  1. 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
  2. Deploy the app and create an instance on a screen.
  3. Set a long value (315 characters):
    screenly edge-app setting set 'bearer_token=<315-character JWT>'
    # Edge App setting successfully set.
    Setting the same value in the dashboard also appears to save.
  4. 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.json contains no entry for the instance under cipher_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_token is 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.secrets for 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.
  • 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-client snap 26.10.0-pi64
  • Edge App built with @screenly/edge-apps 26.8.0

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions