mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-06-29 17:50:00 +02:00
7d300a39c9
- Rename downloadProviderMatchesBuiltIn -> downloadProviderReplacesLegacyProvider - Rename Tidal DASH ffmpeg helpers and lossy format pickers to generic names - Add utils.decryptCTRSegments crypto API + raw/bytes file read path in extension runtime - Update l10n strings/descriptions to drop hardcoded service names - Bump version to 4.5.7+134
486 lines
14 KiB
Go
486 lines
14 KiB
Go
package gobackend
|
|
|
|
import (
|
|
"encoding/json"
|
|
"testing"
|
|
|
|
"github.com/dop251/goja"
|
|
)
|
|
|
|
func newBinaryTestRuntime(t *testing.T, withFilePermission bool) *goja.Runtime {
|
|
t.Helper()
|
|
|
|
ext := &loadedExtension{
|
|
ID: "binary-test-ext",
|
|
Manifest: &ExtensionManifest{
|
|
Name: "binary-test-ext",
|
|
Permissions: ExtensionPermissions{
|
|
File: withFilePermission,
|
|
},
|
|
},
|
|
DataDir: t.TempDir(),
|
|
}
|
|
|
|
runtime := newExtensionRuntime(ext)
|
|
vm := goja.New()
|
|
runtime.RegisterAPIs(vm)
|
|
return vm
|
|
}
|
|
|
|
func decodeJSONResult[T any](t *testing.T, value goja.Value) T {
|
|
t.Helper()
|
|
|
|
var decoded T
|
|
if err := json.Unmarshal([]byte(value.String()), &decoded); err != nil {
|
|
t.Fatalf("failed to decode JSON result: %v", err)
|
|
}
|
|
return decoded
|
|
}
|
|
|
|
func TestExtensionRuntime_FileByteAPIs(t *testing.T) {
|
|
vm := newBinaryTestRuntime(t, true)
|
|
|
|
result, err := vm.RunString(`
|
|
(function() {
|
|
var first = file.writeBytes("bytes.bin", "AAEC", {encoding: "base64", truncate: true});
|
|
if (!first.success) throw new Error(first.error);
|
|
|
|
var second = file.writeBytes("bytes.bin", "0304ff", {encoding: "hex", append: true});
|
|
if (!second.success) throw new Error(second.error);
|
|
|
|
var all = file.readBytes("bytes.bin", {encoding: "hex"});
|
|
if (!all.success) throw new Error(all.error);
|
|
|
|
var slice = file.readBytes("bytes.bin", {offset: 2, length: 2, encoding: "hex"});
|
|
if (!slice.success) throw new Error(slice.error);
|
|
|
|
var tail = file.readBytes("bytes.bin", {offset: 6, length: 4, encoding: "hex"});
|
|
if (!tail.success) throw new Error(tail.error);
|
|
|
|
return JSON.stringify({
|
|
all: all.data,
|
|
slice: slice.data,
|
|
size: all.size,
|
|
sliceBytes: slice.bytes_read,
|
|
sliceEof: slice.eof,
|
|
tailBytes: tail.bytes_read,
|
|
tailEof: tail.eof
|
|
});
|
|
})()
|
|
`)
|
|
if err != nil {
|
|
t.Fatalf("file byte APIs failed: %v", err)
|
|
}
|
|
|
|
decoded := decodeJSONResult[struct {
|
|
All string `json:"all"`
|
|
Slice string `json:"slice"`
|
|
Size int64 `json:"size"`
|
|
SliceBytes int `json:"sliceBytes"`
|
|
SliceEof bool `json:"sliceEof"`
|
|
TailBytes int `json:"tailBytes"`
|
|
TailEof bool `json:"tailEof"`
|
|
}](t, result)
|
|
|
|
if decoded.All != "0001020304ff" {
|
|
t.Fatalf("all = %q", decoded.All)
|
|
}
|
|
if decoded.Slice != "0203" {
|
|
t.Fatalf("slice = %q", decoded.Slice)
|
|
}
|
|
if decoded.Size != 6 {
|
|
t.Fatalf("size = %d", decoded.Size)
|
|
}
|
|
if decoded.SliceBytes != 2 {
|
|
t.Fatalf("slice bytes = %d", decoded.SliceBytes)
|
|
}
|
|
if decoded.SliceEof {
|
|
t.Fatal("slice should not be EOF")
|
|
}
|
|
if decoded.TailBytes != 0 || !decoded.TailEof {
|
|
t.Fatalf("tail read mismatch: bytes=%d eof=%v", decoded.TailBytes, decoded.TailEof)
|
|
}
|
|
}
|
|
|
|
func TestExtensionRuntime_BlockCipherCBCSupportsBlowfish(t *testing.T) {
|
|
vm := newBinaryTestRuntime(t, false)
|
|
|
|
result, err := vm.RunString(`
|
|
(function() {
|
|
var options = {
|
|
algorithm: "blowfish",
|
|
mode: "cbc",
|
|
key: "0123456789ABCDEFF0E1D2C3B4A59687",
|
|
keyEncoding: "hex",
|
|
iv: "0001020304050607",
|
|
ivEncoding: "hex",
|
|
inputEncoding: "hex",
|
|
outputEncoding: "hex",
|
|
padding: "none"
|
|
};
|
|
var enc = utils.encryptBlockCipher("00112233445566778899aabbccddeeff", options);
|
|
if (!enc.success) throw new Error(enc.error);
|
|
var dec = utils.decryptBlockCipher(enc.data, options);
|
|
if (!dec.success) throw new Error(dec.error);
|
|
return JSON.stringify({enc: enc.data, dec: dec.data});
|
|
})()
|
|
`)
|
|
if err != nil {
|
|
t.Fatalf("blowfish block cipher failed: %v", err)
|
|
}
|
|
|
|
decoded := decodeJSONResult[struct {
|
|
Enc string `json:"enc"`
|
|
Dec string `json:"dec"`
|
|
}](t, result)
|
|
|
|
if decoded.Dec != "00112233445566778899aabbccddeeff" {
|
|
t.Fatalf("dec = %q", decoded.Dec)
|
|
}
|
|
if decoded.Enc == decoded.Dec {
|
|
t.Fatal("expected ciphertext to differ from plaintext")
|
|
}
|
|
}
|
|
|
|
func TestExtensionRuntime_BlockCipherCBCSupportsAES(t *testing.T) {
|
|
vm := newBinaryTestRuntime(t, false)
|
|
|
|
result, err := vm.RunString(`
|
|
(function() {
|
|
var options = {
|
|
algorithm: "aes",
|
|
mode: "cbc",
|
|
key: "000102030405060708090a0b0c0d0e0f",
|
|
keyEncoding: "hex",
|
|
iv: "0f0e0d0c0b0a09080706050403020100",
|
|
ivEncoding: "hex",
|
|
inputEncoding: "utf8",
|
|
outputEncoding: "base64",
|
|
padding: "pkcs7"
|
|
};
|
|
var enc = utils.encryptBlockCipher("hello generic cbc", options);
|
|
if (!enc.success) throw new Error(enc.error);
|
|
var dec = utils.decryptBlockCipher(enc.data, {
|
|
algorithm: "aes",
|
|
mode: "cbc",
|
|
key: options.key,
|
|
keyEncoding: options.keyEncoding,
|
|
iv: options.iv,
|
|
ivEncoding: options.ivEncoding,
|
|
inputEncoding: "base64",
|
|
outputEncoding: "utf8",
|
|
padding: "pkcs7"
|
|
});
|
|
if (!dec.success) throw new Error(dec.error);
|
|
return dec.data;
|
|
})()
|
|
`)
|
|
if err != nil {
|
|
t.Fatalf("aes block cipher failed: %v", err)
|
|
}
|
|
|
|
if result.String() != "hello generic cbc" {
|
|
t.Fatalf("unexpected decrypted value: %q", result.String())
|
|
}
|
|
}
|
|
|
|
func TestExtensionRuntime_BlockCipherCTRSupportsAES(t *testing.T) {
|
|
vm := newBinaryTestRuntime(t, false)
|
|
|
|
// NIST SP 800-38A, F.5.1 CTR-AES128.Encrypt test vector.
|
|
// Key: 2b7e151628aed2a6abf7158809cf4f3c
|
|
// Counter: f0f1f2f3f4f5f6f7f8f9fafbfcfdfeff
|
|
// Plaintext: 6bc1bee22e409f96e93d7e117393172a (block 1)
|
|
// Ciphertext: 874d6191b620e3261bef6864990db6ce (block 1)
|
|
result, err := vm.RunString(`
|
|
(function() {
|
|
var options = {
|
|
algorithm: "aes",
|
|
mode: "ctr",
|
|
key: "2b7e151628aed2a6abf7158809cf4f3c",
|
|
keyEncoding: "hex",
|
|
iv: "f0f1f2f3f4f5f6f7f8f9fafbfcfdfeff",
|
|
ivEncoding: "hex",
|
|
inputEncoding: "hex",
|
|
outputEncoding: "hex"
|
|
};
|
|
var enc = utils.encryptBlockCipher("6bc1bee22e409f96e93d7e117393172a", options);
|
|
if (!enc.success) throw new Error(enc.error);
|
|
// CTR is symmetric: decrypt is the same transform as encrypt.
|
|
var dec = utils.decryptBlockCipher(enc.data, options);
|
|
if (!dec.success) throw new Error(dec.error);
|
|
return JSON.stringify({enc: enc.data, dec: dec.data});
|
|
})()
|
|
`)
|
|
if err != nil {
|
|
t.Fatalf("aes ctr block cipher failed: %v", err)
|
|
}
|
|
|
|
decoded := decodeJSONResult[struct {
|
|
Enc string `json:"enc"`
|
|
Dec string `json:"dec"`
|
|
}](t, result)
|
|
|
|
if decoded.Enc != "874d6191b620e3261bef6864990db6ce" {
|
|
t.Fatalf("ctr ciphertext = %q, want NIST vector 874d6191b620e3261bef6864990db6ce", decoded.Enc)
|
|
}
|
|
if decoded.Dec != "6bc1bee22e409f96e93d7e117393172a" {
|
|
t.Fatalf("ctr round-trip dec = %q", decoded.Dec)
|
|
}
|
|
}
|
|
|
|
func TestExtensionRuntime_BlockCipherCTRHandlesNonBlockLength(t *testing.T) {
|
|
vm := newBinaryTestRuntime(t, false)
|
|
|
|
// CTR is a stream mode, so arbitrary (non-16-byte-aligned) input lengths
|
|
// must round-trip without any padding.
|
|
result, err := vm.RunString(`
|
|
(function() {
|
|
var options = {
|
|
algorithm: "aes",
|
|
mode: "ctr",
|
|
key: "000102030405060708090a0b0c0d0e0f",
|
|
keyEncoding: "hex",
|
|
iv: "0f0e0d0c0b0a09080706050403020100",
|
|
ivEncoding: "hex",
|
|
inputEncoding: "utf8",
|
|
outputEncoding: "base64"
|
|
};
|
|
var enc = utils.encryptBlockCipher("stream ctr of odd length", options);
|
|
if (!enc.success) throw new Error(enc.error);
|
|
var dec = utils.decryptBlockCipher(enc.data, {
|
|
algorithm: "aes",
|
|
mode: "ctr",
|
|
key: options.key,
|
|
keyEncoding: options.keyEncoding,
|
|
iv: options.iv,
|
|
ivEncoding: options.ivEncoding,
|
|
inputEncoding: "base64",
|
|
outputEncoding: "utf8"
|
|
});
|
|
if (!dec.success) throw new Error(dec.error);
|
|
return dec.data;
|
|
})()
|
|
`)
|
|
if err != nil {
|
|
t.Fatalf("aes ctr stream length failed: %v", err)
|
|
}
|
|
|
|
if result.String() != "stream ctr of odd length" {
|
|
t.Fatalf("unexpected ctr decrypted value: %q", result.String())
|
|
}
|
|
}
|
|
|
|
func TestExtensionRuntime_BlockCipherCTRRejectsBadIV(t *testing.T) {
|
|
vm := newBinaryTestRuntime(t, false)
|
|
|
|
result, err := vm.RunString(`
|
|
(function() {
|
|
var res = utils.encryptBlockCipher("00112233", {
|
|
algorithm: "aes",
|
|
mode: "ctr",
|
|
key: "000102030405060708090a0b0c0d0e0f",
|
|
keyEncoding: "hex",
|
|
iv: "0001",
|
|
ivEncoding: "hex",
|
|
inputEncoding: "hex",
|
|
outputEncoding: "hex"
|
|
});
|
|
return JSON.stringify({success: res.success, error: res.error || ""});
|
|
})()
|
|
`)
|
|
if err != nil {
|
|
t.Fatalf("aes ctr bad iv eval failed: %v", err)
|
|
}
|
|
|
|
decoded := decodeJSONResult[struct {
|
|
Success bool `json:"success"`
|
|
Error string `json:"error"`
|
|
}](t, result)
|
|
|
|
if decoded.Success {
|
|
t.Fatal("expected failure for undersized CTR iv")
|
|
}
|
|
if decoded.Error == "" {
|
|
t.Fatal("expected error message for undersized CTR iv")
|
|
}
|
|
}
|
|
|
|
func TestExtensionRuntime_DecryptCTRSegmentsMatchesPerSegment(t *testing.T) {
|
|
vm := newBinaryTestRuntime(t, false)
|
|
|
|
// Build a buffer of 3 segments encrypted with distinct 8-byte IVs (CENC
|
|
// style), then verify the batch primitive decrypts all of them in one call,
|
|
// matching what per-segment decryptBlockCipher would produce.
|
|
result, err := vm.RunString(`
|
|
(function() {
|
|
var keyHex = "000102030405060708090a0b0c0d0e0f";
|
|
function b64(bytes){return utils.base64Encode(utils.toHex ? bytes : bytes);}
|
|
|
|
// segment plaintexts (hex) and 8-byte IVs (hex)
|
|
var segs = [
|
|
{ pt: "11111111111111111111", iv: "0000000000000001" },
|
|
{ pt: "2222222222", iv: "0000000000000002" },
|
|
{ pt: "333333333333333333333333", iv: "00000000000000ff" }
|
|
];
|
|
|
|
// Encrypt each segment individually using single-shot CTR with a
|
|
// 16-byte counter (8-byte iv left-aligned), producing ciphertext hex.
|
|
function ivToB64(ivHex){
|
|
// pad 8-byte hex iv to 16 bytes then base64
|
|
var full = ivHex + "00000000000000000000000000000000".slice(ivHex.length);
|
|
return utils.base64Encode(utils.hexToBytes ? utils.hexToBytes(full) : full);
|
|
}
|
|
|
|
var cipherHex = "";
|
|
var offsets = [];
|
|
var off = 0;
|
|
var ivB64s = [];
|
|
for (var i=0;i<segs.length;i++){
|
|
var ivFullHex = (segs[i].iv + "00000000000000000000000000000000").slice(0,32);
|
|
var enc = utils.encryptBlockCipher(segs[i].pt, {
|
|
algorithm:"aes", mode:"ctr", key:keyHex, keyEncoding:"hex",
|
|
iv: ivFullHex, ivEncoding:"hex",
|
|
inputEncoding:"hex", outputEncoding:"hex"
|
|
});
|
|
if(!enc.success) throw new Error("enc seg "+i+": "+enc.error);
|
|
cipherHex += enc.data;
|
|
var sz = segs[i].pt.length/2;
|
|
offsets.push({offset: off, size: sz, ivHex: ivFullHex});
|
|
off += sz;
|
|
}
|
|
|
|
// Now decrypt the whole concatenated buffer in ONE batch call.
|
|
var segments = offsets.map(function(o){
|
|
return { offset:o.offset, size:o.size, iv:o.ivHex };
|
|
});
|
|
var batch = utils.decryptCTRSegments(cipherHex, {
|
|
algorithm:"aes", key:keyHex, keyEncoding:"hex",
|
|
segments: segments, ivEncoding:"hex",
|
|
inputEncoding:"hex", outputEncoding:"hex"
|
|
});
|
|
if(!batch.success) throw new Error("batch: "+batch.error);
|
|
|
|
var expected = "";
|
|
for (var j=0;j<segs.length;j++) expected += segs[j].pt;
|
|
|
|
return JSON.stringify({
|
|
out: batch.data,
|
|
expected: expected,
|
|
processed: batch.segments_processed
|
|
});
|
|
})()
|
|
`)
|
|
if err != nil {
|
|
t.Fatalf("batch CTR eval failed: %v", err)
|
|
}
|
|
|
|
decoded := decodeJSONResult[struct {
|
|
Out string `json:"out"`
|
|
Expected string `json:"expected"`
|
|
Processed int `json:"processed"`
|
|
}](t, result)
|
|
|
|
if decoded.Out != decoded.Expected {
|
|
t.Fatalf("batch decrypt mismatch:\n got=%s\nwant=%s", decoded.Out, decoded.Expected)
|
|
}
|
|
if decoded.Processed != 3 {
|
|
t.Fatalf("segments_processed = %d, want 3", decoded.Processed)
|
|
}
|
|
}
|
|
|
|
func TestExtensionRuntime_DecryptCTRSegmentsRejectsOutOfBounds(t *testing.T) {
|
|
vm := newBinaryTestRuntime(t, false)
|
|
|
|
result, err := vm.RunString(`
|
|
(function() {
|
|
var res = utils.decryptCTRSegments("00112233", {
|
|
algorithm:"aes", key:"000102030405060708090a0b0c0d0e0f", keyEncoding:"hex",
|
|
inputEncoding:"hex", outputEncoding:"hex",
|
|
ivEncoding:"hex",
|
|
segments: [ { offset: 0, size: 99, iv: "00000000000000000000000000000000" } ]
|
|
});
|
|
return JSON.stringify({ success: res.success, error: res.error || "" });
|
|
})()
|
|
`)
|
|
if err != nil {
|
|
t.Fatalf("oob eval failed: %v", err)
|
|
}
|
|
|
|
decoded := decodeJSONResult[struct {
|
|
Success bool `json:"success"`
|
|
Error string `json:"error"`
|
|
}](t, result)
|
|
|
|
if decoded.Success {
|
|
t.Fatal("expected out-of-bounds segment to fail")
|
|
}
|
|
if decoded.Error == "" {
|
|
t.Fatal("expected error message for out-of-bounds segment")
|
|
}
|
|
}
|
|
|
|
func TestExtensionRuntime_DecryptCTRSegmentsRawBytes(t *testing.T) {
|
|
vm := newBinaryTestRuntime(t, false)
|
|
|
|
// Verify the zero-base64 path: pass an ArrayBuffer in, request bytes out,
|
|
// and confirm round-trip correctness against single-shot CTR.
|
|
result, err := vm.RunString(`
|
|
(function() {
|
|
var keyHex = "000102030405060708090a0b0c0d0e0f";
|
|
var ivFullHex = "0000000000000001" + "00000000000000000000000000000000".slice(16);
|
|
|
|
// Plaintext as a Uint8Array of 20 bytes.
|
|
var pt = new Uint8Array(20);
|
|
for (var i = 0; i < pt.length; i++) pt[i] = (i * 7 + 3) & 0xff;
|
|
|
|
// Encrypt single-shot to get ciphertext (hex output for clarity).
|
|
var ptHex = "";
|
|
for (var j = 0; j < pt.length; j++) { var h = pt[j].toString(16); ptHex += (h.length === 1 ? "0" : "") + h; }
|
|
var enc = utils.encryptBlockCipher(ptHex, {
|
|
algorithm:"aes", mode:"ctr", key:keyHex, keyEncoding:"hex",
|
|
iv: ivFullHex, ivEncoding:"hex", inputEncoding:"hex", outputEncoding:"base64"
|
|
});
|
|
if (!enc.success) throw new Error("enc: " + enc.error);
|
|
|
|
// Decode ciphertext base64 into a Uint8Array to feed the raw path.
|
|
var cipherBytes = utils.base64Decode ? null : null;
|
|
// Build ArrayBuffer from base64 via Uint8Array manually:
|
|
var b64 = enc.data;
|
|
var bin = (typeof atob === "function") ? null : null;
|
|
|
|
// Simpler: ask the host to give us bytes by decrypting nothing is hard,
|
|
// so just pass the base64 ciphertext through decryptCTRSegments using
|
|
// base64 input but bytes output, then re-run with bytes input.
|
|
var step1 = utils.decryptCTRSegments(b64, {
|
|
algorithm:"aes", key:keyHex, keyEncoding:"hex",
|
|
segments: [ { offset:0, size:20, iv: ivFullHex } ],
|
|
ivEncoding:"hex", inputEncoding:"base64", outputEncoding:"bytes"
|
|
});
|
|
if (!step1.success) throw new Error("step1: " + step1.error);
|
|
if (typeof step1.data === "string") throw new Error("expected ArrayBuffer output, got string");
|
|
|
|
var outArr = new Uint8Array(step1.data);
|
|
var outHex = "";
|
|
for (var k = 0; k < outArr.length; k++) { var hh = outArr[k].toString(16); outHex += (hh.length === 1 ? "0" : "") + hh; }
|
|
return JSON.stringify({ out: outHex, expected: ptHex, len: outArr.length });
|
|
})()
|
|
`)
|
|
if err != nil {
|
|
t.Fatalf("raw-bytes eval failed: %v", err)
|
|
}
|
|
|
|
decoded := decodeJSONResult[struct {
|
|
Out string `json:"out"`
|
|
Expected string `json:"expected"`
|
|
Len int `json:"len"`
|
|
}](t, result)
|
|
|
|
if decoded.Out != decoded.Expected {
|
|
t.Fatalf("raw-bytes decrypt mismatch:\n got=%s\nwant=%s", decoded.Out, decoded.Expected)
|
|
}
|
|
if decoded.Len != 20 {
|
|
t.Fatalf("output length = %d, want 20", decoded.Len)
|
|
}
|
|
}
|