diff --git a/Source/AaxDecrypter/AaxcDownloadConvertBase.cs b/Source/AaxDecrypter/AaxcDownloadConvertBase.cs index 22279818..8f350cb2 100644 --- a/Source/AaxDecrypter/AaxcDownloadConvertBase.cs +++ b/Source/AaxDecrypter/AaxcDownloadConvertBase.cs @@ -1,5 +1,6 @@ using AAXClean; using System; +using System.Linq; using System.Threading.Tasks; namespace AaxDecrypter @@ -33,20 +34,33 @@ namespace AaxDecrypter { if (DownloadOptions.InputType is FileType.Dash) { + //We may have multiple keys , so use the key whose key ID matches + //the dash files default Key ID. + var keyIds = DownloadOptions.DecryptionKeys.Select(k => new Guid(k.KeyPart1, bigEndian: true)).ToArray(); + var dash = new DashFile(InputFileStream); - dash.SetDecryptionKey(DownloadOptions.AudibleKey, DownloadOptions.AudibleIV); + var kidIndex = Array.IndexOf(keyIds, dash.Tenc.DefaultKID); + + if (kidIndex == -1) + throw new InvalidOperationException($"None of the {keyIds.Length} key IDs match the dash file's default KeyID of {dash.Tenc.DefaultKID}"); + + DownloadOptions.DecryptionKeys[0] = DownloadOptions.DecryptionKeys[kidIndex]; + var keyId = DownloadOptions.DecryptionKeys[kidIndex].KeyPart1; + var key = DownloadOptions.DecryptionKeys[kidIndex].KeyPart2; + + dash.SetDecryptionKey(keyId, key); return dash; } else if (DownloadOptions.InputType is FileType.Aax) { var aax = new AaxFile(InputFileStream); - aax.SetDecryptionKey(DownloadOptions.AudibleKey); + aax.SetDecryptionKey(DownloadOptions.DecryptionKeys[0].KeyPart1); return aax; } else if (DownloadOptions.InputType is FileType.Aaxc) { var aax = new AaxFile(InputFileStream); - aax.SetDecryptionKey(DownloadOptions.AudibleKey, DownloadOptions.AudibleIV); + aax.SetDecryptionKey(DownloadOptions.DecryptionKeys[0].KeyPart1, DownloadOptions.DecryptionKeys[0].KeyPart2); return aax; } else throw new InvalidOperationException($"{nameof(DownloadOptions.InputType)} of '{DownloadOptions.InputType}' is unknown."); diff --git a/Source/AaxDecrypter/AudiobookDownloadBase.cs b/Source/AaxDecrypter/AudiobookDownloadBase.cs index 392c21f1..355b3eca 100644 --- a/Source/AaxDecrypter/AudiobookDownloadBase.cs +++ b/Source/AaxDecrypter/AudiobookDownloadBase.cs @@ -73,11 +73,16 @@ namespace AaxDecrypter AsyncSteps[$"Cleanup"] = CleanupAsync; (bool success, var elapsed) = await AsyncSteps.RunAsync(); + //Stop the downloader so it doesn't keep running in the background. + if (!success) + nfsPersister.Dispose(); + await progressTask; var speedup = DownloadOptions.RuntimeLength / elapsed; Serilog.Log.Information($"Speedup is {speedup:F0}x realtime."); + nfsPersister.Dispose(); return success; async Task reportProgress() @@ -177,7 +182,7 @@ namespace AaxDecrypter FileUtility.SaferDelete(jsonDownloadState); - if (!string.IsNullOrEmpty(DownloadOptions.AudibleKey) && + if (DownloadOptions.DecryptionKeys != null && DownloadOptions.RetainEncryptedFile && DownloadOptions.InputType is AAXClean.FileType fileType) { @@ -188,17 +193,21 @@ namespace AaxDecrypter if (fileType is AAXClean.FileType.Aax) { - await File.WriteAllTextAsync(keyPath, $"ActivationBytes={DownloadOptions.AudibleKey}"); + await File.WriteAllTextAsync(keyPath, $"ActivationBytes={Convert.ToHexString(DownloadOptions.DecryptionKeys[0].KeyPart1)}"); aaxPath = Path.ChangeExtension(tempFilePath, ".aax"); } else if (fileType is AAXClean.FileType.Aaxc) { - await File.WriteAllTextAsync(keyPath, $"Key={DownloadOptions.AudibleKey}{Environment.NewLine}IV={DownloadOptions.AudibleIV}"); + await File.WriteAllTextAsync(keyPath, + $"Key={Convert.ToHexString(DownloadOptions.DecryptionKeys[0].KeyPart1)}{Environment.NewLine}" + + $"IV={Convert.ToHexString(DownloadOptions.DecryptionKeys[0].KeyPart2)}"); aaxPath = Path.ChangeExtension(tempFilePath, ".aaxc"); } else if (fileType is AAXClean.FileType.Dash) { - await File.WriteAllTextAsync(keyPath, $"KeyId={DownloadOptions.AudibleKey}{Environment.NewLine}Key={DownloadOptions.AudibleIV}"); + await File.WriteAllTextAsync(keyPath, + $"KeyId={Convert.ToHexString(DownloadOptions.DecryptionKeys[0].KeyPart1)}{Environment.NewLine}" + + $"Key={Convert.ToHexString(DownloadOptions.DecryptionKeys[0].KeyPart2)}"); aaxPath = Path.ChangeExtension(tempFilePath, ".dash"); } else diff --git a/Source/AaxDecrypter/IDownloadOptions.cs b/Source/AaxDecrypter/IDownloadOptions.cs index 35533c82..be630e85 100644 --- a/Source/AaxDecrypter/IDownloadOptions.cs +++ b/Source/AaxDecrypter/IDownloadOptions.cs @@ -2,15 +2,35 @@ using System; using System.Threading.Tasks; +#nullable enable namespace AaxDecrypter { - public interface IDownloadOptions + public class KeyData + { + public byte[] KeyPart1 { get; } + public byte[]? KeyPart2 { get; } + + public KeyData(byte[] keyPart1, byte[]? keyPart2 = null) + { + KeyPart1 = keyPart1; + KeyPart2 = keyPart2; + } + + public KeyData(string keyPart1, string? keyPart2 = null) + { + ArgumentNullException.ThrowIfNull(keyPart1, nameof(keyPart1)); + KeyPart1 = Convert.FromBase64String(keyPart1); + if (keyPart2 != null) + KeyPart2 = Convert.FromBase64String(keyPart2); + } + } + + public interface IDownloadOptions { event EventHandler DownloadSpeedChanged; string DownloadUrl { get; } string UserAgent { get; } - string AudibleKey { get; } - string AudibleIV { get; } + KeyData[]? DecryptionKeys { get; } TimeSpan RuntimeLength { get; } OutputFormat OutputFormat { get; } bool TrimOutputToChapterLength { get; } @@ -21,14 +41,14 @@ namespace AaxDecrypter long DownloadSpeedBps { get; } ChapterInfo ChapterInfo { get; } bool FixupFile { get; } - string AudibleProductId { get; } - string Title { get; } - string Subtitle { get; } - string Publisher { get; } - string Language { get; } - string SeriesName { get; } + string? AudibleProductId { get; } + string? Title { get; } + string? Subtitle { get; } + string? Publisher { get; } + string? Language { get; } + string? SeriesName { get; } float? SeriesNumber { get; } - NAudio.Lame.LameConfig LameConfig { get; } + NAudio.Lame.LameConfig? LameConfig { get; } bool Downsample { get; } bool MatchSourceBitrate { get; } bool MoveMoovToBeginning { get; } diff --git a/Source/AaxDecrypter/NetworkFileStream.cs b/Source/AaxDecrypter/NetworkFileStream.cs index 8bad5708..1be60545 100644 --- a/Source/AaxDecrypter/NetworkFileStream.cs +++ b/Source/AaxDecrypter/NetworkFileStream.cs @@ -110,14 +110,16 @@ namespace AaxDecrypter #region Downloader /// Update the . - private void OnUpdate() + private void OnUpdate(bool waitForWrite = false) { try { - if (DateTime.UtcNow > NextUpdateTime) + if (waitForWrite || DateTime.UtcNow > NextUpdateTime) { Updated?.Invoke(this, EventArgs.Empty); //JsonFilePersister Will not allow update intervals shorter than 100 milliseconds + //If an update is called less than 100 ms since the last update, persister will + //sleep the thread until 100 ms has elapsed. NextUpdateTime = DateTime.UtcNow.AddMilliseconds(110); } } @@ -305,7 +307,7 @@ namespace AaxDecrypter finally { _downloadedPiece.Set(); - OnUpdate(); + OnUpdate(waitForWrite: true); } } @@ -402,7 +404,7 @@ namespace AaxDecrypter _cancellationSource?.Dispose(); _readFile.Dispose(); _writeFile.Dispose(); - OnUpdate(); + OnUpdate(waitForWrite: true); } disposed = true; diff --git a/Source/AudibleUtilities/Widevine/Cdm.cs b/Source/AudibleUtilities/Widevine/Cdm.cs index b15c820e..99eecf3e 100644 --- a/Source/AudibleUtilities/Widevine/Cdm.cs +++ b/Source/AudibleUtilities/Widevine/Cdm.cs @@ -55,7 +55,7 @@ public class WidevineKey Type = (KeyType)type; Key = key; } - public override string ToString() => $"{Convert.ToHexString(Kid.ToByteArray()).ToLower()}:{Convert.ToHexString(Key).ToLower()}"; + public override string ToString() => $"{Convert.ToHexString(Kid.ToByteArray(bigEndian: true)).ToLower()}:{Convert.ToHexString(Key).ToLower()}"; } public partial class Cdm @@ -192,7 +192,7 @@ public partial class Cdm id = id.Append(new byte[16 - id.Length]); } - keys[i] = new WidevineKey(new Guid(id), keyContainer.Type, keyBytes); + keys[i] = new WidevineKey(new Guid(id,bigEndian: true), keyContainer.Type, keyBytes); } return keys; } diff --git a/Source/FileLiberator/DownloadOptions.Factory.cs b/Source/FileLiberator/DownloadOptions.Factory.cs index d01d9993..94368534 100644 --- a/Source/FileLiberator/DownloadOptions.Factory.cs +++ b/Source/FileLiberator/DownloadOptions.Factory.cs @@ -41,12 +41,31 @@ public partial class DownloadOptions return options; } - private static async Task ChooseContent(Api api, LibraryBook libraryBook, Configuration config) + private class LicenseInfo + { + public DrmType DrmType { get; } + public ContentMetadata ContentMetadata { get; set; } + public KeyData[]? DecryptionKeys { get; } + public LicenseInfo(ContentLicense license, IEnumerable? keys = null) + { + DrmType = license.DrmType; + ContentMetadata = license.ContentMetadata; + DecryptionKeys = keys?.ToArray() ?? ToKeys(license.Voucher); + } + + private static KeyData[]? ToKeys(VoucherDtoV10? voucher) + => voucher is null ? null : [new KeyData(voucher.Key, voucher.Iv)]; + } + + private static async Task ChooseContent(Api api, LibraryBook libraryBook, Configuration config) { var dlQuality = config.FileDownloadQuality == Configuration.DownloadQuality.Normal ? DownloadQuality.Normal : DownloadQuality.High; if (!config.UseWidevine || await Cdm.GetCdmAsync() is not Cdm cdm) - return await api.GetDownloadLicenseAsync(libraryBook.Book.AudibleProductId, dlQuality); + { + var license = await api.GetDownloadLicenseAsync(libraryBook.Book.AudibleProductId, dlQuality); + return new LicenseInfo(license); + } try { @@ -62,6 +81,9 @@ public partial class DownloadOptions config.RequestSpatial, codecChoice); + if (contentLic.DrmType is not DrmType.Widevine) + return new LicenseInfo(contentLic); + using var client = new HttpClient(); using var mpdResponse = await client.GetAsync(contentLic.LicenseResponse); var dash = new MpegDash(mpdResponse.Content.ReadAsStream()); @@ -75,12 +97,7 @@ public partial class DownloadOptions var challenge = session.GetLicenseChallenge(dash); var licenseMessage = await api.WidevineDrmLicense(libraryBook.Book.AudibleProductId, challenge); var keys = session.ParseLicense(licenseMessage); - contentLic.Voucher = new VoucherDtoV10() - { - Key = Convert.ToHexStringLower(keys[0].Kid.ToByteArray()), - Iv = Convert.ToHexStringLower(keys[0].Key) - }; - return contentLic; + return new LicenseInfo(contentLic, keys.Select(k => new KeyData(k.Kid.ToByteArray(bigEndian: true), k.Key))); } catch (Exception ex) { @@ -93,41 +110,20 @@ public partial class DownloadOptions } - private static DownloadOptions BuildDownloadOptions(LibraryBook libraryBook, Configuration config, ContentLicense contentLic) + private static DownloadOptions BuildDownloadOptions(LibraryBook libraryBook, Configuration config, LicenseInfo licInfo) { - //If DrmType is not Adrm or Widevine, the delivered file is an unencrypted mp3. - var outputFormat - = contentLic.DrmType is not DrmType.Adrm and not DrmType.Widevine || - (config.AllowLibationFixup && config.DecryptToLossy && contentLic.ContentMetadata.ContentReference.Codec != "ac-4") - ? OutputFormat.Mp3 - : OutputFormat.M4b; - long chapterStartMs = config.StripAudibleBrandAudio - ? contentLic.ContentMetadata.ChapterInfo.BrandIntroDurationMs + ? licInfo.ContentMetadata.ChapterInfo.BrandIntroDurationMs : 0; - AAXClean.FileType? inputType - = contentLic.DrmType is DrmType.Widevine ? AAXClean.FileType.Dash - : contentLic.DrmType is DrmType.Adrm && contentLic.Voucher?.Key.Length == 8 && contentLic.Voucher?.Iv == null ? AAXClean.FileType.Aax - : contentLic.DrmType is DrmType.Adrm && contentLic.Voucher?.Key.Length == 32 && contentLic.Voucher?.Iv.Length == 32 ? AAXClean.FileType.Aaxc - : null; - - var dlOptions = new DownloadOptions(config, libraryBook, contentLic.ContentMetadata.ContentUrl?.OfflineUrl) + var dlOptions = new DownloadOptions(config, libraryBook, licInfo) { - AudibleKey = contentLic.Voucher?.Key, - AudibleIV = contentLic.Voucher?.Iv, - InputType = inputType, - OutputFormat = outputFormat, - DrmType = contentLic.DrmType, - ContentMetadata = contentLic.ContentMetadata, - LameConfig = outputFormat == OutputFormat.Mp3 ? GetLameOptions(config) : null, ChapterInfo = new AAXClean.ChapterInfo(TimeSpan.FromMilliseconds(chapterStartMs)), - RuntimeLength = TimeSpan.FromMilliseconds(contentLic.ContentMetadata.ChapterInfo.RuntimeLengthMs), + RuntimeLength = TimeSpan.FromMilliseconds(licInfo.ContentMetadata.ChapterInfo.RuntimeLengthMs), }; - dlOptions.LibraryBookDto.Codec = contentLic.ContentMetadata.ContentReference.Codec; - if (TryGetAudioInfo(contentLic.ContentMetadata.ContentUrl, out int? bitrate, out int? sampleRate, out int? channels)) + if (TryGetAudioInfo(licInfo.ContentMetadata.ContentUrl, out int? bitrate, out int? sampleRate, out int? channels)) { dlOptions.LibraryBookDto.BitRate = bitrate; dlOptions.LibraryBookDto.SampleRate = sampleRate; @@ -136,7 +132,7 @@ public partial class DownloadOptions var titleConcat = config.CombineNestedChapterTitles ? ": " : null; var chapters - = flattenChapters(contentLic.ContentMetadata.ChapterInfo.Chapters, titleConcat) + = flattenChapters(licInfo.ContentMetadata.ChapterInfo.Chapters, titleConcat) .OrderBy(c => c.StartOffsetMs) .ToList(); @@ -152,7 +148,7 @@ public partial class DownloadOptions chapLenMs -= chapterStartMs; if (config.StripAudibleBrandAudio && i == chapters.Count - 1) - chapLenMs -= contentLic.ContentMetadata.ChapterInfo.BrandOutroDurationMs; + chapLenMs -= licInfo.ContentMetadata.ChapterInfo.BrandOutroDurationMs; dlOptions.ChapterInfo.AddChapter(chapter.Title, TimeSpan.FromMilliseconds(chapLenMs)); } diff --git a/Source/FileLiberator/DownloadOptions.cs b/Source/FileLiberator/DownloadOptions.cs index e48ce069..3f55abe2 100644 --- a/Source/FileLiberator/DownloadOptions.cs +++ b/Source/FileLiberator/DownloadOptions.cs @@ -9,27 +9,27 @@ using System.IO; using ApplicationServices; using LibationFileManager.Templates; +#nullable enable namespace FileLiberator { public partial class DownloadOptions : IDownloadOptions, IDisposable { - public event EventHandler DownloadSpeedChanged; + public event EventHandler? DownloadSpeedChanged; public LibraryBook LibraryBook { get; } public LibraryBookDto LibraryBookDto { get; } public string DownloadUrl { get; } - public string AudibleKey { get; init; } - public string AudibleIV { get; init; } - public TimeSpan RuntimeLength { get; init; } - public OutputFormat OutputFormat { get; init; } - public ChapterInfo ChapterInfo { get; init; } + public KeyData[]? DecryptionKeys { get; } + public required TimeSpan RuntimeLength { get; init; } + public OutputFormat OutputFormat { get; } + public required ChapterInfo ChapterInfo { get; init; } public string Title => LibraryBook.Book.Title; public string Subtitle => LibraryBook.Book.Subtitle; public string Publisher => LibraryBook.Book.Publisher; public string Language => LibraryBook.Book.Language; - public string AudibleProductId => LibraryBookDto.AudibleProductId; - public string SeriesName => LibraryBookDto.FirstSeries?.Name; + public string? AudibleProductId => LibraryBookDto.AudibleProductId; + public string? SeriesName => LibraryBookDto.FirstSeries?.Name; public float? SeriesNumber => LibraryBookDto.FirstSeries?.Number; - public NAudio.Lame.LameConfig LameConfig { get; init; } + public NAudio.Lame.LameConfig? LameConfig { get; } public string UserAgent => AudibleApi.Resources.Download_User_Agent; public bool TrimOutputToChapterLength => Config.AllowLibationFixup && Config.StripAudibleBrandAudio; public bool StripUnabridged => Config.AllowLibationFixup && Config.StripUnabridged; @@ -41,15 +41,15 @@ namespace FileLiberator public bool Downsample => Config.AllowLibationFixup && Config.LameDownsampleMono; public bool MatchSourceBitrate => Config.AllowLibationFixup && Config.LameMatchSourceBR && Config.LameTargetBitrate; public bool MoveMoovToBeginning => Config.MoveMoovToBeginning; - public AAXClean.FileType? InputType { get; init; } - public AudibleApi.Common.DrmType DrmType { get; init; } - public AudibleApi.Common.ContentMetadata ContentMetadata { get; init; } + public AAXClean.FileType? InputType { get; } + public AudibleApi.Common.DrmType DrmType { get; } + public AudibleApi.Common.ContentMetadata ContentMetadata { get; } public string GetMultipartFileName(MultiConvertFileProperties props) { var baseDir = Path.GetDirectoryName(props.OutputFileName); var extension = Path.GetExtension(props.OutputFileName); - return Templates.ChapterFile.GetFilename(LibraryBookDto, props, baseDir, extension); + return Templates.ChapterFile.GetFilename(LibraryBookDto, props, baseDir!, extension); } public string GetMultipartTitle(MultiConvertFileProperties props) @@ -92,14 +92,38 @@ namespace FileLiberator GC.SuppressFinalize(this); } - private DownloadOptions(Configuration config, LibraryBook libraryBook, [System.Diagnostics.CodeAnalysis.NotNull] string downloadUrl) + private DownloadOptions(Configuration config, LibraryBook libraryBook, LicenseInfo licInfo) { Config = ArgumentValidator.EnsureNotNull(config, nameof(config)); LibraryBook = ArgumentValidator.EnsureNotNull(libraryBook, nameof(libraryBook)); - DownloadUrl = ArgumentValidator.EnsureNotNullOrEmpty(downloadUrl, nameof(downloadUrl)); - // no null/empty check for key/iv. unencrypted files do not have them + ArgumentValidator.EnsureNotNull(licInfo, nameof(licInfo)); + + if (licInfo.ContentMetadata.ContentUrl.OfflineUrl is not string licUrl) + throw new InvalidDataException("Content license doesn't contain an offline Url"); + + DownloadUrl = licUrl; + DecryptionKeys = licInfo.DecryptionKeys; + DrmType = licInfo.DrmType; + ContentMetadata = licInfo.ContentMetadata; + InputType + = licInfo.DrmType is AudibleApi.Common.DrmType.Widevine ? AAXClean.FileType.Dash + : licInfo.DrmType is AudibleApi.Common.DrmType.Adrm && licInfo.DecryptionKeys?.Length == 1 && licInfo.DecryptionKeys[0].KeyPart1.Length == 8 && licInfo.DecryptionKeys[0].KeyPart2 is null ? AAXClean.FileType.Aax + : licInfo.DrmType is AudibleApi.Common.DrmType.Adrm && licInfo.DecryptionKeys?.Length == 1 && licInfo.DecryptionKeys[0].KeyPart1.Length == 32 && licInfo.DecryptionKeys[0].KeyPart2?.Length == 32 ? AAXClean.FileType.Aaxc + : null; + + //If DrmType is not Adrm or Widevine, the delivered file is an unencrypted mp3. + OutputFormat + = licInfo.DrmType is not AudibleApi.Common.DrmType.Adrm and not AudibleApi.Common.DrmType.Widevine || + (config.AllowLibationFixup && config.DecryptToLossy && licInfo.ContentMetadata.ContentReference.Codec != Ac4Codec) + ? OutputFormat.Mp3 + : OutputFormat.M4b; + + LameConfig = OutputFormat == OutputFormat.Mp3 ? GetLameOptions(config) : null; + + // no null/empty check for key/iv. unencrypted files do not have them LibraryBookDto = LibraryBook.ToDto(); + LibraryBookDto.Codec = licInfo.ContentMetadata.ContentReference.Codec; cancellation = config