- Add authentication scripts with SubtleCrypto password encryption - Add sourcemap extraction pipeline (update-urls, download-sourcemaps, extract-sources) - Add Playwright API interception script for monetization endpoints - Document two-step auth flow with JWT tokens and dual cookies - Move extracted source from root to anyclip/ directory - Add project configuration (.env.example, .gitignore, CLAUDE.md)
153 lines
3.7 KiB
TypeScript
153 lines
3.7 KiB
TypeScript
/**
|
|
* Reimplementation of string-crypto v2 using SubtleCrypto (Web Crypto API)
|
|
* Works in browsers, Bun, Deno, Cloudflare Workers, etc.
|
|
*/
|
|
|
|
const KEYLEN = 256; // bits
|
|
|
|
async function deriveKey(
|
|
password: string,
|
|
salt: string = "s41t",
|
|
iterations: number = 1
|
|
): Promise<CryptoKey> {
|
|
const enc = new TextEncoder();
|
|
|
|
// Import password as raw key material
|
|
const keyMaterial = await crypto.subtle.importKey(
|
|
"raw",
|
|
enc.encode(password),
|
|
"PBKDF2",
|
|
false,
|
|
["deriveKey"]
|
|
);
|
|
|
|
// Derive AES-GCM key using PBKDF2
|
|
return crypto.subtle.deriveKey(
|
|
{
|
|
name: "PBKDF2",
|
|
salt: enc.encode(salt),
|
|
iterations,
|
|
hash: "SHA-512",
|
|
},
|
|
keyMaterial,
|
|
{ name: "AES-GCM", length: KEYLEN },
|
|
false,
|
|
["encrypt", "decrypt"]
|
|
);
|
|
}
|
|
|
|
export async function encryptString(
|
|
str: string,
|
|
password: string
|
|
): Promise<string> {
|
|
const key = await deriveKey(password);
|
|
const enc = new TextEncoder();
|
|
|
|
// Generate random IV
|
|
const iv = crypto.getRandomValues(new Uint8Array(16));
|
|
|
|
// Encrypt
|
|
const encrypted = await crypto.subtle.encrypt(
|
|
{ name: "AES-GCM", iv },
|
|
key,
|
|
enc.encode(str)
|
|
);
|
|
|
|
// SubtleCrypto appends 16-byte auth tag, but v2 string-crypto ignores it
|
|
// Strip the last 16 bytes to match v2 behavior
|
|
const encryptedWithoutTag = new Uint8Array(encrypted).slice(0, -16);
|
|
|
|
// Convert to base64, then to hex (v2 quirk)
|
|
const encryptedBase64 = btoa(
|
|
String.fromCharCode(...encryptedWithoutTag)
|
|
);
|
|
const encryptedHex = Buffer.from(encryptedBase64).toString("hex");
|
|
|
|
// IV to hex
|
|
const ivHex = Array.from(iv)
|
|
.map((b) => b.toString(16).padStart(2, "0"))
|
|
.join("");
|
|
|
|
return `${ivHex}:${encryptedHex}`;
|
|
}
|
|
|
|
export async function decryptString(
|
|
encryptedStr: string,
|
|
password: string
|
|
): Promise<string> {
|
|
const key = await deriveKey(password);
|
|
const [ivHex, encryptedHex] = encryptedStr.split(":");
|
|
|
|
// Hex to IV
|
|
const iv = new Uint8Array(
|
|
ivHex.match(/.{2}/g)!.map((byte) => parseInt(byte, 16))
|
|
);
|
|
|
|
// Hex to base64 to bytes
|
|
const encryptedBase64 = Buffer.from(encryptedHex, "hex").toString();
|
|
const encryptedBytes = Uint8Array.from(atob(encryptedBase64), (c) =>
|
|
c.charCodeAt(0)
|
|
);
|
|
|
|
// Decrypt
|
|
const decrypted = await crypto.subtle.decrypt(
|
|
{ name: "AES-GCM", iv },
|
|
key,
|
|
encryptedBytes
|
|
);
|
|
|
|
return new TextDecoder().decode(decrypted);
|
|
}
|
|
|
|
// Test
|
|
if (import.meta.main) {
|
|
// Load .env
|
|
const envFile = Bun.file(".env");
|
|
if (await envFile.exists()) {
|
|
const envContent = await envFile.text();
|
|
for (const line of envContent.split("\n")) {
|
|
const [key, ...valueParts] = line.split("=");
|
|
if (key && valueParts.length) {
|
|
process.env[key.trim()] = valueParts.join("=").trim();
|
|
}
|
|
}
|
|
}
|
|
|
|
const email = process.env.ANYCLIP_EMAIL || process.env.ANYCLIP_USER;
|
|
const plaintext = process.env.ANYCLIP_PASSWORD;
|
|
|
|
if (!email || !plaintext) {
|
|
console.error("Set ANYCLIP_EMAIL and ANYCLIP_PASSWORD in .env");
|
|
process.exit(1);
|
|
}
|
|
|
|
const salt = "$2b$04$wwky7rvtr6BFNaCqntwyie";
|
|
const encrypted = await encryptString(plaintext, salt);
|
|
console.log("Encrypted:", encrypted);
|
|
|
|
// Test with API
|
|
console.log("\nTesting with AnyClip API...");
|
|
const response = await fetch(
|
|
"https://videomanager-api.anyclip.com/public/auth/login",
|
|
{
|
|
method: "POST",
|
|
headers: {
|
|
Accept: "application/json",
|
|
"Content-Type": "application/json",
|
|
},
|
|
body: JSON.stringify({
|
|
email,
|
|
password: encrypted,
|
|
}),
|
|
}
|
|
);
|
|
|
|
console.log("Status:", response.status);
|
|
if (response.ok) {
|
|
console.log("✅ Works with SubtleCrypto!");
|
|
} else {
|
|
const text = await response.text();
|
|
console.log("Response:", text);
|
|
}
|
|
}
|