feat: initial commit

This commit is contained in:
Jixun 2021-12-19 18:19:25 +00:00
commit 80dc75d6a4
16 changed files with 8128 additions and 0 deletions

7
.gitignore vendored Normal file
View File

@ -0,0 +1,7 @@
*.flac
*.ogg
*.mp3
coverage/
node_modules/
*.tgz

3
.npmignore Normal file
View File

@ -0,0 +1,3 @@
*
!src/**/*
src/**/__test__/

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2021-2021 unlock-music<https://github.com/unlock-music>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

7
README.md Normal file
View File

@ -0,0 +1,7 @@
# JOOX-Crypto
A package to decrypt joox encrypted file.
Supported varient:
- `E!04` - encryption ver 4

21
joox-decrypt Normal file
View File

@ -0,0 +1,21 @@
#!/usr/bin/env node
const DecryptorV4 = require("./src/crypto/DecryptorV4");
const fs = require("fs");
if (process.argv.length < 5) {
console.info(
"Usage: %s <uuidv2> <encrypted_path> <decrypted_path>",
process.argv[1]
);
return;
}
const [, , uuid, inputPath, outputPath] = process.argv;
const inputBuffer = fs.readFileSync(inputPath);
const decryptor = new DecryptorV4(uuid);
const result = decryptor.decryptFile(inputBuffer);
const outputHandle = fs.openSync(outputPath, "w");
for (const block of result) {
fs.writeSync(outputHandle, block);
}

7747
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

37
package.json Normal file
View File

@ -0,0 +1,37 @@
{
"name": "joox-crypto",
"type": "commonjs",
"version": "0.0.1",
"description": "Crypto library to decrypt joox encrypted music files.",
"main": "src/index.js",
"types": "src/index.d.ts",
"bin": {
"joox-decrypt": "joox-decrypt"
},
"scripts": {
"test": "jest -w 50% src",
"prepare": "simple-git-hooks"
},
"keywords": [
"joox"
],
"author": "Jixun",
"license": "MIT",
"devDependencies": {
"@types/jest": "^27.0.3",
"jest": "^27.4.5",
"lint-staged": "^12.1.3",
"prettier": "2.5.1",
"simple-git-hooks": "^2.7.0"
},
"dependencies": {
"crypto-js": "^4.1.1"
},
"lint-staged": {
"*.{js,css,md}": "prettier --write"
},
"simple-git-hooks": {
"pre-commit": "npx lint-staged",
"pre-push": "npm test"
}
}

55
src/crypto/AES.js Normal file
View File

@ -0,0 +1,55 @@
const pbkdf2 = require("crypto-js/pbkdf2");
require("crypto-js/hmac-sha1");
const AES = require("crypto-js/aes");
const PKCS7 = require("crypto-js/pad-pkcs7");
const ModeECB = require("crypto-js/mode-ecb");
const C = require("crypto-js/core");
const { Uint8ArrayEncoder } = require("./utils");
const KEY_SIZE = 16;
const defaultSalt = new Uint8Array([
0xa4, 0x0b, 0xc8, 0x34, 0xd6, 0x95, 0xf3, 0x13, 0x23, 0x23, 0x43, 0x23, 0x54,
0x63, 0x83, 0xf3,
]);
/**
* Derive AES Secret key using uuid2
* @param {string} uuid2 User's uuid v2 (generated on app first startup)
* @param {Uint8Array} [salt = null] Salt used to generate aes key.
*/
function getAESSecretKey(uuid2, salt = null) {
const key = pbkdf2(uuid2, Uint8ArrayEncoder.parse(salt || defaultSalt), {
iterations: 1000,
hasher: C.algo.SHA1,
});
return Uint8ArrayEncoder.stringify(key).slice(0, KEY_SIZE);
}
/**
*
* @param {Uint8Array} key
* @param {Uint8Array} block
* @returns {Uint8Array} decrypted data
*/
function decryptAESBlock(key, block) {
const result = AES.decrypt(
{
ciphertext: Uint8ArrayEncoder.parse(block),
},
Uint8ArrayEncoder.parse(key),
{
mode: ModeECB,
padding: PKCS7,
}
);
return Uint8ArrayEncoder.stringify(result);
}
module.exports = {
getAESSecretKey,
decryptAESBlock,
};

53
src/crypto/DecryptorV4.js Normal file
View File

@ -0,0 +1,53 @@
const { Uint8ArrayEncoder } = require("./utils");
const { getAESSecretKey, decryptAESBlock } = require("./AES");
class DecryptorV4 {
static BLOCK_SIZE = 0x100000 /* data size */ + 0x10 /* padding */;
/**
* Derive key from a given seed (user uuid 2)
* @param {string} encryptionSeed
*/
constructor(encryptionSeed) {
this.aesKey = getAESSecretKey(encryptionSeed, null);
}
/**
* Detect if encryption is supported.
* @param {Uint8Array} fileBody File body for detection
*/
static detect(fileBody) {
const magic = Uint8ArrayEncoder.toUTF8(fileBody.slice(0, 4));
return magic === "E!04";
}
/**
* Decrypt a given file.
* @param {Uint8Array} fileBody file body
*/
decryptFile(fileBody) {
if (!DecryptorV4.detect(fileBody)) {
throw new Error("file is not using joox v4");
}
const view = new DataView(fileBody.buffer, 4, 8);
const size = (view.getUint32(0, false) << 32) | view.getUint32(4, false);
if (size < 0) {
throw new RangeError("unable to decode size");
}
const blocks = [];
let bytesToDecrypt = fileBody.length;
let i = /* magic */ 4 + /* orig_size */ 8;
while (bytesToDecrypt > 0) {
const blockSize = Math.min(DecryptorV4.BLOCK_SIZE, bytesToDecrypt);
const block = fileBody.subarray(i, i + blockSize);
const blockDecrypted = decryptAESBlock(this.aesKey, block);
blocks.push(blockDecrypted);
i += blockSize;
bytesToDecrypt -= blockSize;
}
return blocks;
}
}
module.exports = DecryptorV4;

View File

@ -0,0 +1,10 @@
const { getAESSecretKey } = require("../AES");
describe("crypto/AES.js", () => {
it("should be able to derive key", () => {
const key = getAESSecretKey("00000000000000000000000000000000", null);
expect(Buffer.from(key).toString("hex")).toEqual(
"1340ff4af507d725ddaf217dfc38b95f"
);
});
});

View File

@ -0,0 +1,13 @@
const DecryptorV4 = require("../DecryptorV4");
const fs = require("fs");
const { Uint8ArrayEncoder, mergeUint8Array } = require("../utils");
const { toUTF8 } = Uint8ArrayEncoder;
describe("crypto/DecryptorV4", () => {
it("should be able to decode v4 file", () => {
const data = fs.readFileSync(__dirname + "/fixture/v4_hello.bin");
const decryptor = new DecryptorV4("00000000000000000000000000000000");
const result = decryptor.decryptFile(data);
expect(toUTF8(mergeUint8Array(result))).toEqual("Hello World");
});
});

Binary file not shown.

View File

@ -0,0 +1,28 @@
const WordArray = require("crypto-js/lib-typedarrays");
const { Uint8ArrayEncoder } = require("../utils");
describe("utils/Uint8Encoder", () => {
describe("#parse", () => {
it("should be able to parse bytes correctly (aligned 4 bytes)", () => {
const result = Uint8ArrayEncoder.parse(new Uint8Array([1, 2, 3, 4]));
expect(result.words).toEqual([0x01020304]);
});
it("should be able to parse bytes correctly (unaligned)", () => {
const result = Uint8ArrayEncoder.parse(
new Uint8Array([1, 2, 3, 4, 0xff, 0x7f])
);
expect(result.words).toEqual([0x01020304, 0xff7f0000 | 0]);
});
});
describe("#stringify", () => {
it("should be able to decode to Uint8Array (aligned)", () => {
const result = Uint8ArrayEncoder.stringify(
WordArray.create([0x11223344, 0x55667788])
);
expect(result).toEqual(
new Uint8Array([0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88])
);
});
});
});

103
src/crypto/utils.js Normal file
View File

@ -0,0 +1,103 @@
/**
* Merge multiple Uint8Array to one.
* @param {Uint8Array[]} array uint8 array
* @returns {Uint8Array}
*/
function mergeUint8Array(array) {
let length = 0;
array.forEach((item) => {
length += item.length;
});
let mergedArray = new Uint8Array(length);
let offset = 0;
array.forEach((item) => {
mergedArray.set(item, offset);
offset += item.length;
});
return mergedArray;
}
const WordArray = require("crypto-js/lib-typedarrays");
const C = require("crypto-js/core");
const Uint8ArrayEncoder = {
/**
* Converts a word array to a Uint8Array.
*
* @param wordArray The word array.
*
* @return The Uint8Array.
*
* @example
*
* var uint8Array = CryptoJS.enc.Uint8Array.stringify(wordArray);
*/
stringify: function (wordArray) {
const words = wordArray.words;
let bytesToEncode = wordArray.sigBytes;
// Convert
const result = new Uint8Array(bytesToEncode);
let i = 0;
while (bytesToEncode >= 4) {
const word = words[i / 4];
result[i + 0] = (word >>> 24) & 0xff;
result[i + 1] = (word >>> 16) & 0xff;
result[i + 2] = (word >>> 8) & 0xff;
result[i + 3] = (word >>> 0) & 0xff;
i += 4;
bytesToEncode -= 4;
}
if (bytesToEncode > 0) {
const word = words[i / 4];
switch (bytesToEncode) {
case 3:
result[i + 2] = (word >>> 8) & 0xff;
case 2:
result[i + 1] = (word >>> 16) & 0xff;
case 1:
result[i + 0] = (word >>> 24) & 0xff;
}
}
return result;
},
/**
* Converts a Uint8Array to a word array.
*
* @param hexStr The Uint8Array.
*
* @return The word array.
*
* @example
*
* var wordArray = CryptoJS.enc.Uint8Array.parse(uint8Array);
*/
parse: function (blob) {
return WordArray.create(blob);
},
/**
* Converts a Uint8Array to a UTF-8 string.
*
* @param hexStr The Uint8Array.
*
* @return The UTF-8 string.
*
* @example
*
* var str = CryptoJS.enc.Uint8Array.toUTF8(uint8Array);
*/
toUTF8: (uint8Array) => {
return C.enc.Utf8.stringify(Uint8ArrayEncoder.parse(uint8Array));
},
};
module.exports = {
mergeUint8Array,
Uint8ArrayEncoder,
};

12
src/index.d.ts vendored Normal file
View File

@ -0,0 +1,12 @@
declare interface Decryptor {
decryptFile(data: Uint8Array): Uint8Array[];
}
/**
* Initialise a decryptor with given seed + auto detection
* @param data Input Data (used for detection)
* @param uuid UUID retrieved from App's private storage.
*/
declare function jooxFactory(data: Uint8Array, uuid: string): Decryptor | null;
export = jooxFactory;

11
src/index.js Normal file
View File

@ -0,0 +1,11 @@
const DecryptorV4 = require("./crypto/DecryptorV4");
function jooxFactory(fileBody, seed) {
if (DecryptorV4.detect(fileBody)) {
return DecryptorV4(seed);
}
throw new Error("input file not supported or invalid");
}
module.exports = jooxFactory;