mirror of
https://github.com/markuryy/shark.git
synced 2025-12-13 12:01:01 +00:00
fix: use rust blowfish instead of node
The JavaScript blowfish-node library had a critical bug where it would sometimes return 2047 bytes instead of 2048 during decryption, causing byte alignment issues that corrupted FLAC audio at specific intervals (~every 32/82 seconds). Changes: - Add Rust dependencies: blowfish, md5, byteorder - Implement new module in Rust with proper Blowfish CBC - Add decryption Tauri command - Update frontend to call Rust decryption instead of JavaScript - Remove buggy JavaScript blowfish implementation - Update decryption algorithm (6144-byte windows)
This commit is contained in:
@@ -5,7 +5,6 @@
|
||||
|
||||
import { ecb } from '@noble/ciphers/aes.js';
|
||||
import { bytesToHex, hexToBytes, utf8ToBytes } from '@noble/ciphers/utils.js';
|
||||
import Blowfish from 'blowfish-node';
|
||||
|
||||
/**
|
||||
* MD5 hash implementation
|
||||
@@ -184,53 +183,8 @@ export function ecbDecrypt(key: string, data: string): string {
|
||||
return new TextDecoder().decode(decrypted);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate Blowfish key for track decryption
|
||||
*/
|
||||
export function generateBlowfishKey(trackId: string): Uint8Array {
|
||||
const SECRET = 'g4el58wc0zvf9na1';
|
||||
const idMd5 = md5(trackId);
|
||||
const bfKey = new Uint8Array(16);
|
||||
|
||||
for (let i = 0; i < 16; i++) {
|
||||
bfKey[i] = idMd5.charCodeAt(i) ^ idMd5.charCodeAt(i + 16) ^ SECRET.charCodeAt(i);
|
||||
}
|
||||
|
||||
return bfKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrypt a chunk using Blowfish CBC
|
||||
*/
|
||||
export function decryptChunk(chunk: Uint8Array, blowfishKey: Uint8Array): Uint8Array {
|
||||
try {
|
||||
// Convert Uint8Array to the format blowfish-node expects
|
||||
const bf = new Blowfish(blowfishKey, Blowfish.MODE.CBC, Blowfish.PADDING.NULL);
|
||||
|
||||
// Set IV as Uint8Array
|
||||
const iv = new Uint8Array([0, 1, 2, 3, 4, 5, 6, 7]);
|
||||
bf.setIv(iv);
|
||||
|
||||
// Decode and return as Uint8Array
|
||||
const decrypted = bf.decode(chunk, Blowfish.TYPE.UINT8_ARRAY);
|
||||
|
||||
// Verify decryption worked
|
||||
if (!decrypted || decrypted.length === 0) {
|
||||
throw new Error('Decryption returned empty result');
|
||||
}
|
||||
|
||||
return new Uint8Array(decrypted);
|
||||
} catch (error) {
|
||||
// Only log the first few errors to avoid spam
|
||||
if (Math.random() < 0.01) { // Log ~1% of errors
|
||||
console.error('Error decrypting chunk (sample):', error, {
|
||||
chunkLength: chunk.length,
|
||||
keyLength: blowfishKey.length
|
||||
});
|
||||
}
|
||||
return chunk; // Return original if decryption fails
|
||||
}
|
||||
}
|
||||
// Note: Blowfish decryption is now handled by Rust (see src-tauri/src/deezer_crypto.rs)
|
||||
// The decrypt_deezer_track Tauri command should be used instead
|
||||
|
||||
/**
|
||||
* Generate stream path for download URL
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
import { fetch } from '@tauri-apps/plugin-http';
|
||||
import { writeFile, mkdir, remove, rename, exists } from '@tauri-apps/plugin-fs';
|
||||
import { generateBlowfishKey, decryptChunk } from './crypto';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { generateTrackPath } from './paths';
|
||||
import { tagAudioFile } from './tagger';
|
||||
import { downloadCover, saveCoverToAlbumFolder } from './imageDownload';
|
||||
@@ -86,8 +86,13 @@ export async function downloadTrack(
|
||||
let decryptedData: Uint8Array;
|
||||
|
||||
if (isCrypted) {
|
||||
console.log('Decrypting track...');
|
||||
decryptedData = await decryptTrackData(encryptedData, track.id.toString());
|
||||
console.log('Decrypting track using Rust...');
|
||||
// Call Rust decryption function
|
||||
const decrypted = await invoke<number[]>('decrypt_deezer_track', {
|
||||
data: Array.from(encryptedData),
|
||||
trackId: track.id.toString()
|
||||
});
|
||||
decryptedData = new Uint8Array(decrypted);
|
||||
} else {
|
||||
decryptedData = encryptedData;
|
||||
}
|
||||
@@ -182,72 +187,6 @@ export async function downloadTrack(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrypt track data using Blowfish CBC
|
||||
* Deezer encrypts every 3rd chunk of 2048 bytes
|
||||
*/
|
||||
async function decryptTrackData(data: Uint8Array, trackId: string): Promise<Uint8Array> {
|
||||
const chunkSize = 2048;
|
||||
const blowfishKey = generateBlowfishKey(trackId);
|
||||
const result: Uint8Array[] = [];
|
||||
|
||||
let offset = 0;
|
||||
let chunkIndex = 0;
|
||||
|
||||
// Skip initial padding (null bytes before actual data)
|
||||
while (offset < data.length && data[offset] === 0) {
|
||||
offset++;
|
||||
}
|
||||
|
||||
// If we found padding, check if next bytes are 'ftyp' (MP4) or 'ID3' (MP3) or 'fLaC' (FLAC)
|
||||
if (offset > 0 && offset + 8 < data.length) {
|
||||
const header = String.fromCharCode(...data.slice(offset + 4, offset + 8));
|
||||
if (header === 'ftyp') {
|
||||
// Skip the null padding
|
||||
result.push(data.slice(0, offset));
|
||||
} else {
|
||||
// Reset if we didn't find expected header
|
||||
offset = 0;
|
||||
}
|
||||
} else {
|
||||
offset = 0;
|
||||
}
|
||||
|
||||
while (offset < data.length) {
|
||||
const remainingBytes = data.length - offset;
|
||||
const currentChunkSize = Math.min(chunkSize, remainingBytes);
|
||||
const chunk = data.slice(offset, offset + currentChunkSize);
|
||||
|
||||
// Decrypt every 3rd chunk (0, 3, 6, 9, ...)
|
||||
if (chunkIndex % 3 === 0 && chunk.length === chunkSize) {
|
||||
try {
|
||||
const decrypted = decryptChunk(chunk, blowfishKey);
|
||||
result.push(decrypted);
|
||||
} catch (error) {
|
||||
console.error('Error decrypting chunk:', error);
|
||||
result.push(chunk); // Use original if decryption fails
|
||||
}
|
||||
} else {
|
||||
result.push(chunk);
|
||||
}
|
||||
|
||||
offset += currentChunkSize;
|
||||
chunkIndex++;
|
||||
}
|
||||
|
||||
// Combine all chunks
|
||||
const totalLength = result.reduce((sum, chunk) => sum + chunk.length, 0);
|
||||
const combined = new Uint8Array(totalLength);
|
||||
let position = 0;
|
||||
|
||||
for (const chunk of result) {
|
||||
combined.set(chunk, position);
|
||||
position += chunk.length;
|
||||
}
|
||||
|
||||
return combined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a track file already exists
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user