Skip to content

src/voucher.ts

38 lines · 1.3 KB · TypeScript
Raw
import { aes128CbcDecrypt } from './aes.ts'

const encoder = new TextEncoder()

/**
 * Decrypt an AAXC license voucher (key/iv) bound to this device and customer.
 * Same algorithm as mkb79/Audible `decrypt_voucher_from_licenserequest`.
 */
export async function decryptVoucher(input: {
	deviceType: string
	serial: string
	customerId: string
	asin: string
	encryptedVoucher: string
}): Promise<{ key?: string; iv?: string; rules?: unknown }> {
	const digest = new Uint8Array(
		await crypto.subtle.digest(
			'SHA-256',
			encoder.encode(`${input.deviceType}${input.serial}${input.customerId}${input.asin}`),
		),
	)
	const binary = atob(input.encryptedVoucher)
	const cipherBytes = new Uint8Array(binary.length)
	for (let i = 0; i < binary.length; i++) cipherBytes[i] = binary.charCodeAt(i)
	const plaintext = new TextDecoder().decode(
		aes128CbcDecrypt(digest.subarray(0, 16), digest.subarray(16), cipherBytes),
	)
	try {
		const parsed = JSON.parse(plaintext) as { key?: string; iv?: string; rules?: unknown }
		return { key: parsed.key, iv: parsed.iv, rules: parsed.rules }
	} catch {
		const match = plaintext.match(
			/^\{\s*"key"\s*:\s*"(?<key>[^"]*)"\s*,\s*"iv"\s*:\s*"(?<iv>[^"]*)"/,
		)
		if (match?.groups) return { key: match.groups.key, iv: match.groups.iv }
		throw new Error('Failed to parse decrypted Audible voucher JSON.')
	}
}