mirror of
https://git.unlock-music.dev/um/web.git
synced 2025-11-05 11:11:25 +00:00
all: format with prettier
(cherry picked from commit cad5b4d7deba4fbe4a40a17306ce49d3b2f13139)
This commit is contained in:
@@ -1,56 +1,73 @@
|
||||
import {fromByteArray as Base64Encode} from "base64-js";
|
||||
import { fromByteArray as Base64Encode } from 'base64-js';
|
||||
|
||||
export const IXAREA_API_ENDPOINT = "https://um-api.ixarea.com"
|
||||
export const IXAREA_API_ENDPOINT = 'https://um-api.ixarea.com';
|
||||
|
||||
export interface UpdateInfo {
|
||||
Found: boolean
|
||||
HttpsFound: boolean
|
||||
Version: string
|
||||
URL: string
|
||||
Detail: string
|
||||
Found: boolean;
|
||||
HttpsFound: boolean;
|
||||
Version: string;
|
||||
URL: string;
|
||||
Detail: string;
|
||||
}
|
||||
|
||||
export async function checkUpdate(version: string): Promise<UpdateInfo> {
|
||||
const resp = await fetch(IXAREA_API_ENDPOINT + "/music/app-version", {
|
||||
method: "POST",
|
||||
headers: {"Content-Type": "application/json"},
|
||||
body: JSON.stringify({"Version": version})
|
||||
});
|
||||
return await resp.json();
|
||||
const resp = await fetch(IXAREA_API_ENDPOINT + '/music/app-version', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ Version: version }),
|
||||
});
|
||||
return await resp.json();
|
||||
}
|
||||
|
||||
export function reportKeyUsage(keyData: Uint8Array, maskData: number[], filename: string, format: string, title: string, artist?: string, album?: string) {
|
||||
return fetch(IXAREA_API_ENDPOINT + "/qmcmask/usage", {
|
||||
method: "POST",
|
||||
headers: {"Content-Type": "application/json"},
|
||||
body: JSON.stringify({
|
||||
Mask: Base64Encode(new Uint8Array(maskData)), Key: Base64Encode(keyData),
|
||||
Artist: artist, Title: title, Album: album, Filename: filename, Format: format
|
||||
}),
|
||||
})
|
||||
export function reportKeyUsage(
|
||||
keyData: Uint8Array,
|
||||
maskData: number[],
|
||||
filename: string,
|
||||
format: string,
|
||||
title: string,
|
||||
artist?: string,
|
||||
album?: string,
|
||||
) {
|
||||
return fetch(IXAREA_API_ENDPOINT + '/qmcmask/usage', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
Mask: Base64Encode(new Uint8Array(maskData)),
|
||||
Key: Base64Encode(keyData),
|
||||
Artist: artist,
|
||||
Title: title,
|
||||
Album: album,
|
||||
Filename: filename,
|
||||
Format: format,
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
interface KeyInfo {
|
||||
Matrix44: string
|
||||
Matrix44: string;
|
||||
}
|
||||
|
||||
export async function queryKeyInfo(keyData: Uint8Array, filename: string, format: string): Promise<KeyInfo> {
|
||||
const resp = await fetch(IXAREA_API_ENDPOINT + "/qmcmask/query", {
|
||||
method: "POST",
|
||||
headers: {"Content-Type": "application/json"},
|
||||
body: JSON.stringify({Format: format, Key: Base64Encode(keyData), Filename: filename, Type: 44}),
|
||||
});
|
||||
return await resp.json();
|
||||
const resp = await fetch(IXAREA_API_ENDPOINT + '/qmcmask/query', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ Format: format, Key: Base64Encode(keyData), Filename: filename, Type: 44 }),
|
||||
});
|
||||
return await resp.json();
|
||||
}
|
||||
|
||||
export interface CoverInfo {
|
||||
Id: string
|
||||
Type: number
|
||||
Id: string;
|
||||
Type: number;
|
||||
}
|
||||
|
||||
export async function queryAlbumCover(title: string, artist?: string, album?: string): Promise<CoverInfo> {
|
||||
const endpoint = IXAREA_API_ENDPOINT + "/music/qq-cover"
|
||||
const params = new URLSearchParams([["Title", title], ["Artist", artist ?? ""], ["Album", album ?? ""]])
|
||||
const resp = await fetch(`${endpoint}?${params.toString()}`)
|
||||
return await resp.json()
|
||||
const endpoint = IXAREA_API_ENDPOINT + '/music/qq-cover';
|
||||
const params = new URLSearchParams([
|
||||
['Title', title],
|
||||
['Artist', artist ?? ''],
|
||||
['Album', album ?? ''],
|
||||
]);
|
||||
const resp = await fetch(`${endpoint}?${params.toString()}`);
|
||||
return await resp.json();
|
||||
}
|
||||
|
||||
@@ -4,74 +4,67 @@
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in https://go.dev/LICENSE.
|
||||
|
||||
import {TeaCipher} from "@/utils/tea";
|
||||
import { TeaCipher } from '@/utils/tea';
|
||||
|
||||
test('key size', () => {
|
||||
// prettier-ignore
|
||||
const testKey = new Uint8Array([
|
||||
0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77,
|
||||
0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff,
|
||||
0x00,
|
||||
])
|
||||
expect(() => new TeaCipher(testKey.slice(0, 16))).not.toThrow();
|
||||
|
||||
test("key size", () => {
|
||||
const testKey = new Uint8Array([
|
||||
0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77,
|
||||
0x88, 0x99, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF,
|
||||
0x00
|
||||
])
|
||||
expect(() => new TeaCipher(testKey.slice(0, 16)))
|
||||
.not.toThrow()
|
||||
|
||||
expect(() => new TeaCipher(testKey))
|
||||
.toThrow()
|
||||
|
||||
expect(() => new TeaCipher(testKey.slice(0, 15)))
|
||||
.toThrow()
|
||||
|
||||
})
|
||||
expect(() => new TeaCipher(testKey)).toThrow();
|
||||
|
||||
expect(() => new TeaCipher(testKey.slice(0, 15))).toThrow();
|
||||
});
|
||||
|
||||
// prettier-ignore
|
||||
const teaTests = [
|
||||
// These were sourced from https://github.com/froydnj/ironclad/blob/master/testing/test-vectors/tea.testvec
|
||||
{
|
||||
rounds: TeaCipher.numRounds,
|
||||
key: new Uint8Array([
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]),
|
||||
plainText: new Uint8Array([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]),
|
||||
cipherText: new Uint8Array([0x41, 0xea, 0x3a, 0x0a, 0x94, 0xba, 0xa9, 0x40]),
|
||||
},
|
||||
{
|
||||
rounds: TeaCipher.numRounds,
|
||||
key: new Uint8Array([
|
||||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
|
||||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff]),
|
||||
plainText: new Uint8Array([0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff]),
|
||||
cipherText: new Uint8Array([0x31, 0x9b, 0xbe, 0xfb, 0x01, 0x6a, 0xbd, 0xb2]),
|
||||
},
|
||||
{
|
||||
rounds: 16,
|
||||
key: new Uint8Array([
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]),
|
||||
plainText: new Uint8Array([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]),
|
||||
cipherText: new Uint8Array([0xed, 0x28, 0x5d, 0xa1, 0x45, 0x5b, 0x33, 0xc1]),
|
||||
},
|
||||
]
|
||||
// These were sourced from https://github.com/froydnj/ironclad/blob/master/testing/test-vectors/tea.testvec
|
||||
{
|
||||
rounds: TeaCipher.numRounds,
|
||||
key: new Uint8Array([
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
]),
|
||||
plainText: new Uint8Array([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]),
|
||||
cipherText: new Uint8Array([0x41, 0xea, 0x3a, 0x0a, 0x94, 0xba, 0xa9, 0x40]),
|
||||
},
|
||||
{
|
||||
rounds: TeaCipher.numRounds,
|
||||
key: new Uint8Array([
|
||||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
|
||||
]),
|
||||
plainText: new Uint8Array([0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff]),
|
||||
cipherText: new Uint8Array([0x31, 0x9b, 0xbe, 0xfb, 0x01, 0x6a, 0xbd, 0xb2]),
|
||||
},
|
||||
{
|
||||
rounds: 16,
|
||||
key: new Uint8Array([
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
]),
|
||||
plainText: new Uint8Array([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]),
|
||||
cipherText: new Uint8Array([0xed, 0x28, 0x5d, 0xa1, 0x45, 0x5b, 0x33, 0xc1]),
|
||||
},
|
||||
];
|
||||
|
||||
test("rounds", () => {
|
||||
const tt = teaTests[0];
|
||||
expect(() => new TeaCipher(tt.key, tt.rounds - 1))
|
||||
.toThrow()
|
||||
})
|
||||
test('rounds', () => {
|
||||
const tt = teaTests[0];
|
||||
expect(() => new TeaCipher(tt.key, tt.rounds - 1)).toThrow();
|
||||
});
|
||||
|
||||
test('encrypt & decrypt', () => {
|
||||
for (const tt of teaTests) {
|
||||
const c = new TeaCipher(tt.key, tt.rounds);
|
||||
|
||||
test("encrypt & decrypt", () => {
|
||||
for (const tt of teaTests) {
|
||||
const c = new TeaCipher(tt.key, tt.rounds)
|
||||
const buf = new Uint8Array(8);
|
||||
const bufView = new DataView(buf.buffer);
|
||||
|
||||
const buf = new Uint8Array(8)
|
||||
const bufView = new DataView(buf.buffer)
|
||||
|
||||
c.encrypt(bufView, new DataView(tt.plainText.buffer))
|
||||
expect(buf).toStrictEqual(tt.cipherText)
|
||||
|
||||
c.decrypt(bufView, new DataView(tt.cipherText.buffer))
|
||||
expect(buf).toStrictEqual(tt.plainText)
|
||||
}
|
||||
})
|
||||
c.encrypt(bufView, new DataView(tt.plainText.buffer));
|
||||
expect(buf).toStrictEqual(tt.cipherText);
|
||||
|
||||
c.decrypt(bufView, new DataView(tt.cipherText.buffer));
|
||||
expect(buf).toStrictEqual(tt.plainText);
|
||||
}
|
||||
});
|
||||
|
||||
100
src/utils/tea.ts
100
src/utils/tea.ts
@@ -15,68 +15,66 @@
|
||||
// where compatibility with legacy systems, not security, is the goal.
|
||||
|
||||
export class TeaCipher {
|
||||
// BlockSize is the size of a TEA block, in bytes.
|
||||
static readonly BlockSize = 8;
|
||||
// BlockSize is the size of a TEA block, in bytes.
|
||||
static readonly BlockSize = 8;
|
||||
|
||||
// KeySize is the size of a TEA key, in bytes.
|
||||
static readonly KeySize = 16;
|
||||
// KeySize is the size of a TEA key, in bytes.
|
||||
static readonly KeySize = 16;
|
||||
|
||||
// delta is the TEA key schedule constant.
|
||||
static readonly delta = 0x9e3779b9;
|
||||
// delta is the TEA key schedule constant.
|
||||
static readonly delta = 0x9e3779b9;
|
||||
|
||||
// numRounds 64 is the standard number of rounds in TEA.
|
||||
static readonly numRounds = 64;
|
||||
// numRounds 64 is the standard number of rounds in TEA.
|
||||
static readonly numRounds = 64;
|
||||
|
||||
k0: number
|
||||
k1: number
|
||||
k2: number
|
||||
k3: number
|
||||
rounds: number
|
||||
k0: number;
|
||||
k1: number;
|
||||
k2: number;
|
||||
k3: number;
|
||||
rounds: number;
|
||||
|
||||
constructor(key: Uint8Array, rounds: number = TeaCipher.numRounds) {
|
||||
if (key.length != 16) {
|
||||
throw Error("incorrect key size")
|
||||
}
|
||||
if ((rounds & 1) != 0) {
|
||||
throw Error("odd number of rounds specified")
|
||||
}
|
||||
|
||||
const k = new DataView(key.buffer)
|
||||
this.k0 = k.getUint32(0, false)
|
||||
this.k1 = k.getUint32(4, false)
|
||||
this.k2 = k.getUint32(8, false)
|
||||
this.k3 = k.getUint32(12, false)
|
||||
this.rounds = rounds
|
||||
constructor(key: Uint8Array, rounds: number = TeaCipher.numRounds) {
|
||||
if (key.length != 16) {
|
||||
throw Error('incorrect key size');
|
||||
}
|
||||
if ((rounds & 1) != 0) {
|
||||
throw Error('odd number of rounds specified');
|
||||
}
|
||||
|
||||
const k = new DataView(key.buffer);
|
||||
this.k0 = k.getUint32(0, false);
|
||||
this.k1 = k.getUint32(4, false);
|
||||
this.k2 = k.getUint32(8, false);
|
||||
this.k3 = k.getUint32(12, false);
|
||||
this.rounds = rounds;
|
||||
}
|
||||
|
||||
encrypt(dst: DataView, src: DataView) {
|
||||
encrypt(dst: DataView, src: DataView) {
|
||||
let v0 = src.getUint32(0, false);
|
||||
let v1 = src.getUint32(4, false);
|
||||
|
||||
let v0 = src.getUint32(0, false)
|
||||
let v1 = src.getUint32(4, false)
|
||||
|
||||
let sum = 0
|
||||
for (let i = 0; i < this.rounds / 2; i++) {
|
||||
sum = sum + TeaCipher.delta
|
||||
v0 += ((v1 << 4) + this.k0) ^ (v1 + sum) ^ ((v1 >>> 5) + this.k1)
|
||||
v1 += ((v0 << 4) + this.k2) ^ (v0 + sum) ^ ((v0 >>> 5) + this.k3)
|
||||
}
|
||||
|
||||
dst.setUint32(0, v0, false)
|
||||
dst.setUint32(4, v1, false)
|
||||
let sum = 0;
|
||||
for (let i = 0; i < this.rounds / 2; i++) {
|
||||
sum = sum + TeaCipher.delta;
|
||||
v0 += ((v1 << 4) + this.k0) ^ (v1 + sum) ^ ((v1 >>> 5) + this.k1);
|
||||
v1 += ((v0 << 4) + this.k2) ^ (v0 + sum) ^ ((v0 >>> 5) + this.k3);
|
||||
}
|
||||
|
||||
decrypt(dst: DataView, src: DataView) {
|
||||
let v0 = src.getUint32(0, false)
|
||||
let v1 = src.getUint32(4, false)
|
||||
dst.setUint32(0, v0, false);
|
||||
dst.setUint32(4, v1, false);
|
||||
}
|
||||
|
||||
let sum = TeaCipher.delta * this.rounds / 2
|
||||
for (let i = 0; i < this.rounds / 2; i++) {
|
||||
v1 -= ((v0 << 4) + this.k2) ^ (v0 + sum) ^ ((v0 >>> 5) + this.k3)
|
||||
v0 -= ((v1 << 4) + this.k0) ^ (v1 + sum) ^ ((v1 >>> 5) + this.k1)
|
||||
sum -= TeaCipher.delta
|
||||
}
|
||||
dst.setUint32(0, v0, false)
|
||||
dst.setUint32(4, v1, false)
|
||||
decrypt(dst: DataView, src: DataView) {
|
||||
let v0 = src.getUint32(0, false);
|
||||
let v1 = src.getUint32(4, false);
|
||||
|
||||
let sum = (TeaCipher.delta * this.rounds) / 2;
|
||||
for (let i = 0; i < this.rounds / 2; i++) {
|
||||
v1 -= ((v0 << 4) + this.k2) ^ (v0 + sum) ^ ((v0 >>> 5) + this.k3);
|
||||
v0 -= ((v1 << 4) + this.k0) ^ (v1 + sum) ^ ((v1 >>> 5) + this.k1);
|
||||
sum -= TeaCipher.delta;
|
||||
}
|
||||
dst.setUint32(0, v0, false);
|
||||
dst.setUint32(4, v1, false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,79 +1,80 @@
|
||||
import {DecryptResult} from "@/decrypt/entity";
|
||||
import {FileSystemDirectoryHandle} from "@/shims-fs";
|
||||
import { DecryptResult } from '@/decrypt/entity';
|
||||
import { FileSystemDirectoryHandle } from '@/shims-fs';
|
||||
|
||||
export enum FilenamePolicy {
|
||||
ArtistAndTitle,
|
||||
TitleOnly,
|
||||
TitleAndArtist,
|
||||
SameAsOriginal,
|
||||
ArtistAndTitle,
|
||||
TitleOnly,
|
||||
TitleAndArtist,
|
||||
SameAsOriginal,
|
||||
}
|
||||
|
||||
export const FilenamePolicies: { key: FilenamePolicy, text: string }[] = [
|
||||
{key: FilenamePolicy.ArtistAndTitle, text: "歌手-歌曲名"},
|
||||
{key: FilenamePolicy.TitleOnly, text: "歌曲名"},
|
||||
{key: FilenamePolicy.TitleAndArtist, text: "歌曲名-歌手"},
|
||||
{key: FilenamePolicy.SameAsOriginal, text: "同源文件名"},
|
||||
]
|
||||
export const FilenamePolicies: { key: FilenamePolicy; text: string }[] = [
|
||||
{ key: FilenamePolicy.ArtistAndTitle, text: '歌手-歌曲名' },
|
||||
{ key: FilenamePolicy.TitleOnly, text: '歌曲名' },
|
||||
{ key: FilenamePolicy.TitleAndArtist, text: '歌曲名-歌手' },
|
||||
{ key: FilenamePolicy.SameAsOriginal, text: '同源文件名' },
|
||||
];
|
||||
|
||||
export function GetDownloadFilename(data: DecryptResult, policy: FilenamePolicy): string {
|
||||
switch (policy) {
|
||||
case FilenamePolicy.TitleOnly:
|
||||
return `${data.title}.${data.ext}`;
|
||||
case FilenamePolicy.TitleAndArtist:
|
||||
return `${data.title} - ${data.artist}.${data.ext}`;
|
||||
case FilenamePolicy.SameAsOriginal:
|
||||
return `${data.rawFilename}.${data.ext}`;
|
||||
default:
|
||||
case FilenamePolicy.ArtistAndTitle:
|
||||
return `${data.artist} - ${data.title}.${data.ext}`;
|
||||
}
|
||||
switch (policy) {
|
||||
case FilenamePolicy.TitleOnly:
|
||||
return `${data.title}.${data.ext}`;
|
||||
case FilenamePolicy.TitleAndArtist:
|
||||
return `${data.title} - ${data.artist}.${data.ext}`;
|
||||
case FilenamePolicy.SameAsOriginal:
|
||||
return `${data.rawFilename}.${data.ext}`;
|
||||
default:
|
||||
case FilenamePolicy.ArtistAndTitle:
|
||||
return `${data.artist} - ${data.title}.${data.ext}`;
|
||||
}
|
||||
}
|
||||
|
||||
export async function DirectlyWriteFile(data: DecryptResult, policy: FilenamePolicy, dir: FileSystemDirectoryHandle) {
|
||||
let filename = GetDownloadFilename(data, policy)
|
||||
// prevent filename exist
|
||||
try {
|
||||
await dir.getFileHandle(filename)
|
||||
filename = `${new Date().getTime()} - ${filename}`
|
||||
} catch (e) {
|
||||
}
|
||||
const file = await dir.getFileHandle(filename, {create: true})
|
||||
const w = await file.createWritable()
|
||||
await w.write(data.blob)
|
||||
await w.close()
|
||||
|
||||
let filename = GetDownloadFilename(data, policy);
|
||||
// prevent filename exist
|
||||
try {
|
||||
await dir.getFileHandle(filename);
|
||||
filename = `${new Date().getTime()} - ${filename}`;
|
||||
} catch (e) {}
|
||||
const file = await dir.getFileHandle(filename, { create: true });
|
||||
const w = await file.createWritable();
|
||||
await w.write(data.blob);
|
||||
await w.close();
|
||||
}
|
||||
|
||||
export function DownloadBlobMusic(data: DecryptResult, policy: FilenamePolicy) {
|
||||
const a = document.createElement('a');
|
||||
a.href = data.file;
|
||||
a.download = GetDownloadFilename(data, policy)
|
||||
document.body.append(a);
|
||||
a.click();
|
||||
a.remove();
|
||||
const a = document.createElement('a');
|
||||
a.href = data.file;
|
||||
a.download = GetDownloadFilename(data, policy);
|
||||
document.body.append(a);
|
||||
a.click();
|
||||
a.remove();
|
||||
}
|
||||
|
||||
export function RemoveBlobMusic(data: DecryptResult) {
|
||||
URL.revokeObjectURL(data.file);
|
||||
if (data.picture?.startsWith("blob:")) {
|
||||
URL.revokeObjectURL(data.picture);
|
||||
}
|
||||
URL.revokeObjectURL(data.file);
|
||||
if (data.picture?.startsWith('blob:')) {
|
||||
URL.revokeObjectURL(data.picture);
|
||||
}
|
||||
}
|
||||
|
||||
export class DecryptQueue {
|
||||
private readonly pending: (() => Promise<void>)[];
|
||||
private readonly pending: (() => Promise<void>)[];
|
||||
|
||||
constructor() {
|
||||
this.pending = []
|
||||
}
|
||||
constructor() {
|
||||
this.pending = [];
|
||||
}
|
||||
|
||||
queue(fn: () => Promise<void>) {
|
||||
this.pending.push(fn)
|
||||
this.consume()
|
||||
}
|
||||
queue(fn: () => Promise<void>) {
|
||||
this.pending.push(fn);
|
||||
this.consume();
|
||||
}
|
||||
|
||||
private consume() {
|
||||
const fn = this.pending.shift()
|
||||
if (fn) fn().then(() => this.consume).catch(console.error)
|
||||
}
|
||||
private consume() {
|
||||
const fn = this.pending.shift();
|
||||
if (fn)
|
||||
fn()
|
||||
.then(() => this.consume)
|
||||
.catch(console.error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {expose} from "threads/worker";
|
||||
import {CommonDecrypt} from "@/decrypt/common";
|
||||
import { expose } from 'threads/worker';
|
||||
import { CommonDecrypt } from '@/decrypt/common';
|
||||
|
||||
expose(CommonDecrypt)
|
||||
expose(CommonDecrypt);
|
||||
|
||||
Reference in New Issue
Block a user