Data Types

Encrypted URLs

Share URLs to private files with obfuscated query parameters and file paths — no HTTP headers required.

An Encrypted URL is a Bytescale file URL with a single enc parameter that contains an AES-GCM encrypted version of the original query string. When Bytescale receives the request, it decrypts the query string, authorizes access to any private files using the API key that was used to encrypt the query string, and then serves the request as normal using the decrypted query string.

The original query string must include the expiration time (exp parameter) and can optionally include the file path (path parameter); the latter allows you to move the file path (i.e. the file identifier) from the URL path (which remains in plaintext) to the query string (which will be encrypted).

Encrypted URLs include a single query string parameter:

  • enc: An encrypted query string that includes exp, and optionally path and other transformation parameters.

Example:

https://upcdn.io/W142hJk/image/photo.jpg?enc=1.BMCyGyFk.6Kfhg5wI8t9AOaY1.tP2Q5uCTv

When a request is made, Bytescale will:

  1. Decrypt the enc parameter using the associated API key.

  2. Check the exp timestamp to ensure the URL hasn’t expired.

  3. Apply the API key’s permissions at request time.

To create an Encrypted URL:

  1. Construct the full URL including query parameters (e.g. transformations).

  2. Add an exp parameter specifying when the URL should expire (in Unix epoch seconds or milliseconds).

    This is a required parameter and must be less than 7 days from now.

  3. Optionally move the "Bytescale file path" (the part after /raw/ or /image/, etc.) into a new path query string parameter.

    This allows you to encrypt the file path by moving it from the URL path (which will remain in plaintext) to the query string (which will become encrypted). If a path parameter is present on the query string, it will override any file path present on the URL path.

    The path parameter's value must begin with a / and must not encode forward-slashes (i.e. it must use / not %2F).

    E.g. "https://upcdn.io/W142hJk/raw/?path=/uploads/photo.jpg"

  4. Sign the full URL to deterministically generate the IV (AES-GCM requires a 96-bit IV).

  5. Encrypt the query string using AES-GCM with the IV and your API key’s "Secure URL Key" (available in the API Key's settings in the Bytescale Dashboard).

  6. Construct the final enc parameter as <enc-version>.<api-key-id>.<iv>.<ciphertext>.

    E.g. "upcdn.io/W142hJk/raw/example.jpg?enc=1.BMCyGyFk.6Kfhg5wI8t9AOaY1.tP2Q5uCTv"

    The <enc-version> must always be 1.

    The <iv> and <ciphertext> must be Base64Url encoded (not standard Base64).

Tip: Signed URLs have a simpler implementation and offer similar benefits.

// Credentials (from: Bytescale Dashboard > Security > API Keys > Edit > Secure URLs)
const apiKeyId = "****dk7S"; // ⬅️️ REPLACE with your API Key ID
const secureUrlKey = "********************AA=="; // ⬅️️ REPLACE with your Secure URL Key
/**
* ⬇️️ REPLACE with a constant JWK return value.
*
* Currently, this method generates a new JWK on each call, which will cause CDN cache misses.
*
* To fix:
* 1. Run this function once to log the JWK.
* 2. Replace the method body with a hardcoded return value, or better, load from config (see commented-out code below).
*/
async function getIvSigningKeyJwk() {
// Example:
// return {"key_ops":["sign","verify"],"ext":true,"kty":"oct","k":"eHtVy8zSNbg2xDLaCONo_cxSMoHCgvaCmgyK5DgNtDEVujFLg8Rnhw_EuHKjHmg1uvZoNhAkaMWXrlCF-oVCEw","alg":"HS512"}
const key = await crypto.subtle.generateKey(
{
name: "HMAC",
hash: { name: "SHA-512" },
length: 512, // in bits
},
true, // extractable (so we can export it)
["sign", "verify"]
);
const jwk = await crypto.subtle.exportKey("jwk", key);
console.log(`New JWK used for IV (FIX ME): ${JSON.stringify(jwk)}`)
return jwk;
}
/**
* Encrypts a Bytescale File URL using AES-GCM with deterministic IVs (to improve cache hits).
*
* @param fileUrl E.g. "https://upcdn.io/W142hJk/image/example/photo.jpg?w=800&h=600"
* Must be a valid URL (i.e. must already be URL encoded)."
* @param encryptFilePath If true, moves the file path into the query string as `path`, allowing it to be encrypted (and thus hidden).
* If false, the path remains in plaintext.
* @param ttlSeconds How long (from now) the URL should remain valid (default: 10 minutes).
* @param ttlIncrementSize Rounds expiration to this interval to improve CDN caching (default: 60 seconds).
* @returns A URL with the query string encrypted in an `enc` parameter.
*/
async function getEncryptedUrl(fileUrl, encryptFilePath, ttlSeconds = 600, ttlIncrementSize = 60) {
if (!/^(https?:)?\/\//i.test(fileUrl)) {
throw new Error("Invalid URL: must start with http://, https://, or //");
}
if (ttlSeconds < 1 || ttlSeconds > 604800) {
throw new Error("Invalid TTL: must be between 1 second and 7 days.");
}
if (ttlIncrementSize < 1 || ttlIncrementSize > 604800) {
throw new Error("Invalid TTL increment: must be between 1 second and 7 days.");
}
// 1. Import encryption key (AES-GCM)
const keyData = Buffer.from(secureUrlKey, "base64"); // Bytescale URL Security Keys are Base64-encoded and decode to a valid AES key length.
const encryptionKey = await crypto.subtle.importKey("raw", keyData, "AES-GCM", false, ["encrypt"]);
// 2. Import IV signing key (for deterministic IV generation)
const ivSigningKey = await crypto.subtle.importKey(
"jwk",
await getIvSigningKeyJwk(),
{ name: "HMAC", hash: "SHA-512" },
false,
["sign"]
);
// 3. Calculate rounded expiration time (the 'exp' parameter is mandatory for encrypted URLs)
const now = Math.floor(Date.now() / 1000);
const expiration = Math.ceil((now + ttlSeconds) / ttlIncrementSize) * ttlIncrementSize;
const expirationParam = `exp=${expiration}`;
const urlWithExpiration = fileUrl.includes("?") ? `${fileUrl}&${expirationParam}` : `${fileUrl}?${expirationParam}`;
// 4. Extract file path & query string
const [baseUrl, query] = urlWithExpiration.split("?");
const match = baseUrl.match(/((?:https?:)?\/\/(?:[^\/]+\/){3})(.*)$/);
if (!match) {
throw new Error("Invalid Bytescale URL format");
}
const fileUrlPrefix = match[1]; // E.g. "https://upcdn.io/FW25aki/image/"
const filePath = "/" + match[2]; // E.g. "/example/photo.jpg"
// 5. Optionally move the file path into the query string
const finalBaseUrl = encryptFilePath ? fileUrlPrefix : baseUrl;
const encodeForQuery = str => str.replace(/[&=]/g, c => `%${c.charCodeAt(0).toString(16).toUpperCase()}`);
const finalQuery = encryptFilePath ? `${query}&path=${encodeForQuery(filePath)}` : query;
const queryData = new TextEncoder().encode(finalQuery);
// 6. Generate a deterministic IV using HMAC(urlWithExpiration)
const ivInput = new TextEncoder().encode(urlWithExpiration);
const fullIv = new Uint8Array(await crypto.subtle.sign("HMAC", ivSigningKey, ivInput));
const iv = fullIv.slice(0, 12); // 96-bit IV required by AES-GCM
const ivBase64 = Buffer.from(iv).toString("base64url");
// 7. Encrypt the query string using AES-GCM
const ciphertext = await crypto.subtle.encrypt(
{ name: "AES-GCM", iv, tagLength: 128 },
encryptionKey,
queryData
);
const ciphertextBase64 = Buffer.from(ciphertext).toString("base64url");
// 8. Return the final Encrypted URL using the `enc` parameter
const enc = `enc=1.${apiKeyId}.${ivBase64}.${ciphertextBase64}`;
return `${finalBaseUrl}?${enc}`;
}
// Returns: "https://upcdn.io/W142hJk/image/?enc=..."
console.log(
await getEncryptedUrl("https://upcdn.io/W142hJk/image/example.jpg?w=800&h=600", true)
);
  • Security:

    • Encrypted URLs conceal the query string and optionally the file path from users.

    • Permissions are enforced using the API key at request time.

    • Revoking the API key immediately invalidates existing URLs (providing they have not already been served and cached).

    • Short expiration times reduce the risk of leaked links. See Expiring URLs.

    • Only a single enc parameter is allowed on the URL: if other parameters exist, an error will be thrown.

      • This ensures users cannot add additional query string parameters to alter request behavior.

    • The URL query string cannot be modified by the user.

    • The URL path can be modified by the user — but you can prevent changes to the file path (i.e. the file identifier) by using the path query parameter. This overrides any file path in the URL path itself, allowing you to move the file path into the encrypted portion of the URL, where it cannot be tampered with. You can also restrict access to delivery methods (e.g. /raw/, /image/, etc.) via the API key's settings, and if needed, you can use different API keys to encrypt URLs for different delivery methods.

    • Expired URLs and cryptographically-invalid URLs return the same generic error message. This is by design: since the expiration time is part of the encrypted data, Bytescale must decrypt the URL before checking if it’s expired. To prevent attackers from learning whether decryption succeeded, all invalid or expired URLs return the same error.

  • Performance:

    • Deterministic IVs and rounded expiration times help improve CDN cache hits by ensuring URLs for the same resource, generated within a short time window, are identical. This is achieved by aligning their expiration times. The JavaScript implementation above handles expiration rounding using the ttlIncrementSize parameter.

    • All Encrypted URLs that reference the same transformed resource share the same permanent cache entry. In other words, the permanent cache does not vary based on the enc or exp parameters.

Was this section helpful? Yes No

You are using an outdated browser.

This website requires a modern web browser -- the latest versions of these browsers are supported: