← Public packages
@kentcdodds/audible
Personal Audible library app — browser re-auth, library, wishlist, NAS archive stub.
src/voucher.ts
54 lines · 1.8 KB · TypeScriptimport { 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.')
}
}