Sveltekit and private images related: ERROR 9425 Image access denied: sig query-string argument is n

So trying to implement the signed URLs of private images in Sveltekit. I've done absolute minimal to translate the given URL signing solution at https://developers.cloudflare.com/images/cloudflare-images/serve-images/serve-private-images-using-signed-url-tokens But it doesnt seem to work because of : ERROR 9425: Image access denied: sig query-string argument is not properly hex-encoded (must be 64 hex chars)
Serve private images using signed URL tokens · Cloudflare Image Opt...
If an image is marked to require a signed URL, it cannot be accessed without a token unless it is being requested for a variant that is set to always …
5 Replies
boggin
boggin11mo ago
My server endpoint code is below, whereas the literal only two things that I've changed is the return value (turned into string rather than URL), removed comments for the sake of less noise, and then added my env variable import.
import { CF_IMAGES } from "$env/static/private";
const EXPIRATION = (60 * 60 * 24) * 5; // (1 day) * 5

const bufferToHex = buffer =>
[...new Uint8Array(buffer)].map(x => x.toString(16).padStart(2, '0')).join('');

export async function generateSignedUrl(urlR: string) {
const url = new URL(urlR);

const encoder = new TextEncoder();
const secretKeyData = encoder.encode(CF_IMAGES);
const key = await crypto.subtle.importKey(
'raw',
secretKeyData,
{ name: 'HMAC', hash: 'SHA-256' },
false,
['sign']
);

const expiry = Math.floor(Date.now() / 1000) + EXPIRATION;
url.searchParams.set('exp', expiry);


const stringToSign = url.pathname + '?' + url.searchParams.toString();

const mac = await crypto.subtle.sign('HMAC', key, encoder.encode(stringToSign));
const sig = bufferToHex(new Uint8Array(mac).buffer);

url.searchParams.set('sig', sig);

return url.toString();
}
import { CF_IMAGES } from "$env/static/private";
const EXPIRATION = (60 * 60 * 24) * 5; // (1 day) * 5

const bufferToHex = buffer =>
[...new Uint8Array(buffer)].map(x => x.toString(16).padStart(2, '0')).join('');

export async function generateSignedUrl(urlR: string) {
const url = new URL(urlR);

const encoder = new TextEncoder();
const secretKeyData = encoder.encode(CF_IMAGES);
const key = await crypto.subtle.importKey(
'raw',
secretKeyData,
{ name: 'HMAC', hash: 'SHA-256' },
false,
['sign']
);

const expiry = Math.floor(Date.now() / 1000) + EXPIRATION;
url.searchParams.set('exp', expiry);


const stringToSign = url.pathname + '?' + url.searchParams.toString();

const mac = await crypto.subtle.sign('HMAC', key, encoder.encode(stringToSign));
const sig = bufferToHex(new Uint8Array(mac).buffer);

url.searchParams.set('sig', sig);

return url.toString();
}
Can anyone help explain why it would throw that error though I'm following the docs pretty much exactly? When I console log on the server, the url looks fine, but in Postman/Insomnia and in the browser, it throws that error. The function spits out a url like this (manually obfuscated): https://imagedelivery.net/Zwety7eez57My5vN6lsp1g/2b49sa2n-3163-4335-14zt-c49fb388e401/w640?exp=1690438557&sig=c2d1d9342b8785fedaa53a2b03362afc55a951375684a60c256a272de7beb94c Thank you for any help and guidance in advance. I'm perplexed and can't find anything Googling. the sig param does look all hex and there are 64 characters in the returned sig param. ???
Oscar
Oscar9mo ago
@boggin. Did you ever figure this one out? Having exactly the same trouble
boggin
boggin9mo ago
@o852 I got it fixed eventually. I have no clue what was different the second time I tried it that made it work correctly. I remember only changing 2-3 small details to make the code fit with my usage. I'm using this as a function called from a server endpoint on Sveltekit, here's my code:
import { CLOUDFLARE_KEY } from '$env/static/private';
const EXPIRATION = 60 * 60 * 24; // 1 day

const bufferToHex = (buffer) =>
[...new Uint8Array(buffer)].map((x) => x.toString(16).padStart(2, '0')).join('');

export async function generateSignedUrl(url) {
// `url` is a full imagedelivery.net URL
// e.g. https://imagedelivery.net/cheeW4oKsx5ljh8e8BoL2A/bc27a117-9509-446b-8c69-c81bfeac0a01/mobile

const encoder = new TextEncoder();
const secretKeyData = encoder.encode(CLOUDFLARE_KEY);
const key = await crypto.subtle.importKey(
'raw',
secretKeyData,
{ name: 'HMAC', hash: 'SHA-256' },
false,
['sign']
);

// Attach the expiration value to the `url`
const expiry = Math.floor(Date.now() / 1000) + EXPIRATION;
url.searchParams.set('exp', expiry);
// `url` now looks like
// https://imagedelivery.net/cheeW4oKsx5ljh8e8BoL2A/bc27a117-9509-446b-8c69-c81bfeac0a01/mobile?exp=1631289275

const stringToSign = url.pathname + '?' + url.searchParams.toString();
// for example, /cheeW4oKsx5ljh8e8BoL2A/bc27a117-9509-446b-8c69-c81bfeac0a01/mobile?exp=1631289275

// Generate the signature
const mac = await crypto.subtle.sign('HMAC', key, encoder.encode(stringToSign));
const sig = bufferToHex(new Uint8Array(mac).buffer);

// And attach it to the `url`
url.searchParams.set('sig', sig);

return url;
}
import { CLOUDFLARE_KEY } from '$env/static/private';
const EXPIRATION = 60 * 60 * 24; // 1 day

const bufferToHex = (buffer) =>
[...new Uint8Array(buffer)].map((x) => x.toString(16).padStart(2, '0')).join('');

export async function generateSignedUrl(url) {
// `url` is a full imagedelivery.net URL
// e.g. https://imagedelivery.net/cheeW4oKsx5ljh8e8BoL2A/bc27a117-9509-446b-8c69-c81bfeac0a01/mobile

const encoder = new TextEncoder();
const secretKeyData = encoder.encode(CLOUDFLARE_KEY);
const key = await crypto.subtle.importKey(
'raw',
secretKeyData,
{ name: 'HMAC', hash: 'SHA-256' },
false,
['sign']
);

// Attach the expiration value to the `url`
const expiry = Math.floor(Date.now() / 1000) + EXPIRATION;
url.searchParams.set('exp', expiry);
// `url` now looks like
// https://imagedelivery.net/cheeW4oKsx5ljh8e8BoL2A/bc27a117-9509-446b-8c69-c81bfeac0a01/mobile?exp=1631289275

const stringToSign = url.pathname + '?' + url.searchParams.toString();
// for example, /cheeW4oKsx5ljh8e8BoL2A/bc27a117-9509-446b-8c69-c81bfeac0a01/mobile?exp=1631289275

// Generate the signature
const mac = await crypto.subtle.sign('HMAC', key, encoder.encode(stringToSign));
const sig = bufferToHex(new Uint8Array(mac).buffer);

// And attach it to the `url`
url.searchParams.set('sig', sig);

return url;
}
Oscar
Oscar9mo ago
@boggin. thanks for the help... 4 days later - I realize there's a difference between the API key and the key used to serve images... yikes
boggin
boggin9mo ago
That’s exactly what I discovered in my journey too! Thanks cloudflare…