Skip to content
← Public packages

@kentcdodds/audible

Personal Audible library app — browser re-auth, library, wishlist, NAS archive stub.

src/voucher.ts

54 lines · 1.8 KB · TypeScript
import { aes128CbcDecrypt } from './aes.ts'
import type { AudibleAuth } from './types.ts'

const encoder = new TextEncoder()

/**
 * Decrypt AAXC license voucher (key/iv) using device + customer binding.
 * Same algorithm as mkb79/Audible `decrypt_voucher_from_licenserequest`.
 */
export async function decryptVoucher(input: {
	auth: AudibleAuth
	asin: string
	encryptedVoucher: string
}): Promise<{ key?: string; iv?: string; rules?: unknown; raw?: unknown }> {
	const deviceSerial = input.auth.device_info?.device_serial_number
	const deviceType = input.auth.device_info?.device_type
	const customerId = input.auth.customer_info?.user_id
	if (!deviceSerial || !deviceType || !customerId) {
		throw new Error(
			'device_info.device_serial_number, device_info.device_type, and customer_info.user_id are required to decrypt the AAXC voucher.',
		)
	}

	const buf = encoder.encode(
		`${deviceType}${deviceSerial}${customerId}${input.asin}`,
	)
	const digest = new Uint8Array(await crypto.subtle.digest('SHA-256', buf))
	const key = digest.subarray(0, 16)
	const iv = digest.subarray(16)

	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 plaintextBytes = aes128CbcDecrypt(key, iv, cipherBytes)
	const plaintext = new TextDecoder().decode(plaintextBytes)

	try {
		const parsed = JSON.parse(plaintext) as {
			key?: string
			iv?: string
			rules?: unknown
		}
		return { key: parsed.key, iv: parsed.iv, rules: parsed.rules, raw: parsed }
	} 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.')
	}
}