Unable to serve private CF images

Got a SvelteKit app, and I'm trying out CF images. Upload was nice and easy, but actually serving the images is proving to be a bit of a nightmare I've follow the docs here: https://developers.cloudflare.com/images/cloudflare-images/serve-images/serve-private-images-using-signed-url-tokens/ It produces a good looking signature... https://imagedelivery.net/p78SV55WKTCHZ1_ICRgIKg/43b26a16-265b-4dca-8516-f74e83572501/public?exp=1695126191&sig=875063554c4495f3fc173d87b9483e67e11af8df78a3fe57e52b01dd37eb0c5a But visiting the link errors with:
ERROR 9425: Image access denied: check failed: Malformed data found: The URL signature does not match. Expecting SHA-256 HMAC of the path without domain nor query string
So far I've tried troubleshooting: * Checked API token permissions (currently Cloudflare Images:Read, Cloudflare Images:Edit, Account Analytics:Read, Stream:Edit, Stream:Read) * Have tried rolling new token, didn't make a difference * Have verified that the HMAC signature is "correct" * Using CLOUDFLARE_ACCOUNT_ID instead of CLOUDFLARE_ACCOUNT_HASH * Tried without exp query param so it's just signing url.pathname ...but I've run out of ideas... Any advice would be much appreciated
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
Oscar
OscarOP16mo ago
Here's the code, 95% copied from the sample code linked above
const EXPIRATION = 60 * 60 * 24; // 1 day

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

export async function generateSignedUrl(imageId: string) {
const url = new URL(`https://imagedelivery.net/${env.CF_ACCOUNT_HASH}/${imageId}/public`);
const encoder = new TextEncoder();
const secretKeyData = encoder.encode(env.CF_IMAGES_SECRET_KEY);
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', String(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;
}
const EXPIRATION = 60 * 60 * 24; // 1 day

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

export async function generateSignedUrl(imageId: string) {
const url = new URL(`https://imagedelivery.net/${env.CF_ACCOUNT_HASH}/${imageId}/public`);
const encoder = new TextEncoder();
const secretKeyData = encoder.encode(env.CF_IMAGES_SECRET_KEY);
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', String(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;
}
valdeir3194
valdeir31942mo ago
Hello, did you find a solution to this problem, I'm facing this too and so far I haven't found any solution, I use the same function as in the example informing the private url of the image, but I get the same error. const KEY = process.env.NEXT_PUBLIC_TOKEN_CLOUDFLARE; const EXPIRATION = 60 * 60 * 24; const bufferToHex = (buffer: ArrayBuffer): string => Array.from(new Uint8Array(buffer)).map(b => b.toString(16).padStart(2, '0')).join(''); async function generateSignedUrl(imageUrl: string): Promise<string> { const encoder = new TextEncoder(); const secretKeyData = encoder.encode(KEY);
const key = await crypto.subtle.importKey( 'raw', secretKeyData, { name: 'HMAC', hash: 'SHA-256' }, false, ['sign'] ); const expiry = Math.floor(Date.now() / 1000) + EXPIRATION;
const url = new URL(imageUrl); url.searchParams.set('exp', expiry.toString()); const stringToSign = url.pathname; const mac = await crypto.subtle.sign('HMAC', key, encoder.encode(stringToSign)); const sig = bufferToHex(mac); url.searchParams.set('sig', sig); return url.toString(); } export { generateSignedUrl }; const imageUrl = "https://imagedelivery.net/0F1Nj_HMA0zNbUzFz7cUlw/658e1c6b-3efb-457c-8340-a15f48389401/public"; generateSignedUrl(imageUrl) .then(signedUrl => { console.log("URL: ", signedUrl); // Use o signedUrl conforme necessário, por exemplo, em um link de imagem }) .catch(error => { console.error("URL error: ", error); });
Oscar
OscarOP2mo ago
@valdeir3194 I did get it working - but I can't remember what the specific issue was when I raised this issue Let me see if I can find the code for you that I am currently using
Oscar
OscarOP2mo ago
$lib/server/images.ts
Oscar
OscarOP2mo ago
Let me know if that works for you, my phone thinks it's .ts is a video file format for some reason
Want results from more Discord servers?
Add your server