Is Librezam safe?
Librezam sends audio fingerprint samples to an uncontrolled third-party Deno Deploy proxy and embeds recoverable API credentials in its bundle.
When identifying music via the Netease backend, the extension POSTs raw PCM audio data to a Deno Deploy endpoint controlled by a third party (foxrefire), not the extension author or Netease. Separately, ACRCloud API access keys are stored as AES-CBC blobs whose decryption key is derived entirely from bundle-visible material — the extension name and a hardcoded UUID — making them recoverable by anyone with access to the extension files.
AI-generated. Findings may contain errors. Those marked Verified have been manually reviewed.
Publishers can request a review.
Findings
Netease recognition posts audio to a Deno Deploy proxy
Using Librezam's music recognition with the Netease backend turns captured tab or mic audio into mono PCM and posts it to a Deno Deploy host, not a Netease endpoint.
No request body was captured; the body is described from source only.
You start music recognition while the Netease backend is part of the enabled recognition order.
The popup records from the current tab by default, and the microphone flow records from the microphone after permission is granted.
The extension converts the captured audio into PCM bytes and sends them to a Deno Deploy proxy.
The proxy URL is `ncm-recognizer-proxy-0vx43g2d4hq2.foxrefire.deno.net/api`, not an official Netease API host.
| Field | Value | Why it matters | |
|---|---|---|---|
Audio sample | Mono Float32 PCM buffer; a 7.2-second sample at 48 kHz is about 1,382,400 bytes | The sound clip you asked the extension to identify. Can reveal the music, stream, or mic audio present at that time. | |
Recognition backend | netease | This determines which outside service receives the audio sample for matching. | |
Proxy destination | ncm-recognizer-proxy-0vx43g2d4hq2.foxrefire.deno.net | This is where the extension sends the Netease recognition sample before a song result is returned. |
The popup records audio and passes each enabled backend into Recognize
async function startTabRecognition() {
// Initialize UI for recognition
circler.style.opacity = "0"
circler.style.display = "flex"
circler.style.transition = "opacity 0.3s ease"
setTimeout(() => {
circler.style.opacity = "1"
}, 50)
resultTable.style.display = "none"
streamProvidersContainer.style.display = "none"
notification.classList.remove("show", "pulse", "recognizing")
let fallbackRules = await getStorage("fallbackRules")
let times = Object.keys(fallbackRules).map(t => Number(t))
let backendsMap = Object.values(fallbackRules)
const captureMethod = await getStorage("captureMethod")
let tabCaptureAudios = null
if (captureMethod === "tabCapture" && typeof chrome !== 'undefined' && chrome.tabCapture) {
try {
tabCaptureAudios = await recordFromTabCapture(times)
} catch(e) {
console.error("Tab capture failed:", e)
// If error is about tab not playing audio, show noAudioElementsDetected
if (e.message && e.message.includes("not playing audio")) {
showError(t("noAudioElementsDetected"))
} else {
showError(t("songNotRecognized"))
}
return
}
} else {
await recordAudiosInTab(times)
}
for(let backends of backendsMap) {
showStatus(t("listening"))
let audios
if (tabCaptureAudios) {
try {
let audio = await tabCaptureAudios.shift()
audios = audio ? [audio] : []
} catch(e) {
audios = []
}
} else {
audios = await getNextRecorded().then(r => r.filter(a=> a.length))
}
if(!audios.length) {
showError(t("noAudioElementsDetected"))
return
}
for(let backend of backends) {
let isFound = await getResult(audios, backend)
if(isFound) {
return
}
}
}
showError(t("songNotRecognized"))
}
async function getResult(audios, backend) {
for(let audio of audios) {
try{
showStatus(t("queryingWith", [backend]))
let result = await Recognize(audio, backend)
await writeResult(result)
await saveHistory(result)
await writeHistory() // Update history display immediately
banner.classList.remove("blur")
notification.style.display = "none"
circler.style.opacity = "0"
circler.style.transition = "opacity 0.3s ease"
return true
} catch(e) {
console.log(e)
}
}
return false
}async function startMicRecognition() {
try {
// Reset UI with smooth transitions
resultTable.style.opacity = "0"
streamProviders.style.opacity = "0"
setTimeout(() => {
resultTable.style.display = "none"
streamProvidersContainer.style.display = "none"
}, 300)
circler.style.opacity = "0"
circler.style.transition = "opacity 0.3s ease"
setTimeout(() => {
circler.style.opacity = "1"
}, 50)
notification.classList.remove("show", "pulse", "recognizing")
notification.innerText = ""
// Get fallback rules
let fallbackRules = await getStorage("fallbackRules")
let times = Object.keys(fallbackRules).map(t => Number(t))
let backendsMap = Object.values(fallbackRules)
// Record from microphone
let micAudios = await recordFromMicrophone(times)
// Try recognition with fallback
for(let backends of backendsMap) {
showStatus(t("listening"))
let audio = await micAudios.shift()
if(!audio) {
showError(t("noAudioRecordedFromMicrophone"))
return
}
for(let backend of backends) {
let isFound = await getResult([audio], backend)
if(isFound) {
return
}
}
}
showError(t("songNotRecognizedFromMicrophone"))
} catch(e) {
console.error("Microphone recognition error:", e)
showError(t("failedToAccessMicrophone"))
}
}The Netease backend converts the sample and posts it to the proxy
export async function Recognize(audio, backend) {
console.log(audio)
let backendCall = null
switch(backend) {
case "shazam":
backendCall = shazamGuess
break;
case "audd":
backendCall = auddGuess
break
case "acr":
backendCall = acrGuess
break
case "tencent":
backendCall = tencentGuess
break
case "netease":
backendCall = neteaseGuess
break
}
return await backendCall(audio).then(result => addStreamLinks(result))
}export async function neteaseGuess(audio) {
let pcm = await convertToPCM(audio)
let response = await getResponse(pcm)
console.log(JSON.stringify(response))
return {
title: response[0].song.name,
artist: response[0].song?.artists?.[0]?.name,
album: response[0].song.album?.name,
art: response[0].song.album?.picUrl
}
}
async function convertToPCM(audio) {
// Create AudioContext
const audioContext = new AudioContext();
// Convert to ArrayBuffer
const arrayBuffer = (audio instanceof ArrayBuffer) ? audio : new Uint8Array(audio).buffer;
// Decode it
const audioBuffer = await audioContext.decodeAudioData(arrayBuffer);
// Original sampleRates
const inputChannels = audioBuffer.numberOfChannels;
const inputLength = audioBuffer.length;
// ---- Monauralize it ----
const monoData = new Float32Array(inputLength);
for (let i = 0; i < inputLength; i++) {
let sum = 0;
for (let ch = 0; ch < inputChannels; ch++) {
sum += audioBuffer.getChannelData(ch)[i];
}
monoData[i] = sum / inputChannels;
}
return monoData.buffer
}
async function getResponse(pcm) {
let response = await fetch(`https://ncm-recognizer-proxy-0vx43g2d4hq2.foxrefire.deno.net/api`, {
method: "POST",
body: pcm
}).then(r => r.json())
return response
}- ncm-recognizer-proxy-0vx43g2d4hq2.foxrefire.deno.net
Deno Deploy proxy that receives the PCM audio sample for the Netease recognition backend.