feat: initial commit
This commit is contained in:
commit
2eef064281
7
.gitignore
vendored
Normal file
7
.gitignore
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
*.flac
|
||||
*.ogg
|
||||
*.mp3
|
||||
|
||||
coverage/
|
||||
node_modules/
|
||||
*.tgz
|
3
.npmignore
Normal file
3
.npmignore
Normal file
@ -0,0 +1,3 @@
|
||||
*
|
||||
!src/**/*
|
||||
src/**/__test__/
|
21
LICENSE
Normal file
21
LICENSE
Normal 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
7
README.md
Normal 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
21
joox-decrypt
Normal 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
7747
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
37
package.json
Normal file
37
package.json
Normal 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
55
src/crypto/AES.js
Normal 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
53
src/crypto/DecryptorV4.js
Normal 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;
|
10
src/crypto/__test__/AES.test.js
Normal file
10
src/crypto/__test__/AES.test.js
Normal 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"
|
||||
);
|
||||
});
|
||||
});
|
13
src/crypto/__test__/DecryptorV4.test.js
Normal file
13
src/crypto/__test__/DecryptorV4.test.js
Normal 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");
|
||||
});
|
||||
});
|
BIN
src/crypto/__test__/fixture/v4_hello.bin
Normal file
BIN
src/crypto/__test__/fixture/v4_hello.bin
Normal file
Binary file not shown.
28
src/crypto/__test__/utils.test.js
Normal file
28
src/crypto/__test__/utils.test.js
Normal 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
103
src/crypto/utils.js
Normal 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
12
src/index.d.ts
vendored
Normal 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
11
src/index.js
Normal 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;
|
Loading…
Reference in New Issue
Block a user