diff --git a/Source/_Tests/AudibleUtilities.Tests/AccountTests.cs b/Source/_Tests/AudibleUtilities.Tests/AccountTests.cs index 3ee6fb8e..d19c3a1c 100644 --- a/Source/_Tests/AudibleUtilities.Tests/AccountTests.cs +++ b/Source/_Tests/AudibleUtilities.Tests/AccountTests.cs @@ -1,11 +1,15 @@ using AssertionHelper; using AudibleApi; using AudibleApi.Authorization; +using AudibleApi.Cryptography; using AudibleUtilities; using Microsoft.VisualStudio.TestTools.UnitTesting; using Newtonsoft.Json; +using Newtonsoft.Json.Linq; using System; +using System.Collections.Generic; using System.IO; +using System.Linq; [assembly: Parallelize] @@ -604,5 +608,149 @@ public class transactions : AccountsTestBase } } } + +/// +/// Characterization: AccountsSettings.json currently persists Identity tokens as plaintext +/// with no IsEncrypted metadata. Baseline before token-storage encryption work. +/// +[TestClass] +public class LegacyPlaintextIdentityTokens : AccountsTestBase +{ + const string SampleAccessToken = "Atna|_CHAR_ACCESS_"; + const string SampleRefreshToken = "Atnr|_CHAR_REFRESH_"; + const string SampleAdpToken = "{enc:abcdefg}{key:1234}{iv:56789}{name:QURQVG9rZW5FbmNyeXB0aW9uS2V5}{serial:Mg==}"; + const string SampleStoreAuthCookie = "store-auth-cookie-value"; + static readonly DateTime SampleExpires = new(2200, 1, 1, 12, 0, 0, DateTimeKind.Utc); + + const string SamplePrivateKey = @" +-----BEGIN RSA PRIVATE KEY----- +MIIEpgIBAAKCAQEA5nPbGSVDmlEH2tJa6kz/P2HI8IeirhfPHdmi+X/nsb9i3WNf +tmEdZxfK26IValQDXvBH17a1gr0HD6pYse1XsV2w0HxiW1RW+ZnjL8/fzPdkSOb+ +4xKlqRopCueBSdDGgAF06spZ3IeHLfEFOJX4dO1Y73pFBUkA0k53LT12L2Tjay/r +buZHJqIzxmwja7/nkiWL0Xo7UySHtQACYsKEatu6yHBS+cPTlGR/qeUpeJTHwDLP +7ZQ7kWzJGY1mfInYekjlZLsMsWswso3pg1vPyHgxzM2BWhY8m6mlXQ9G/USxBTib +MNuMtpR73XsgamneFCc+Uv1cxw7ofZ41YOOAbQIDAQABAoIBAQDIre8HkKm0Aggj +B7df/TjxCsgenR6PF/Cmf9UqC7XJ1W3UeCrq+NrP4aonZJfdhdeBnyAQuuyJMu6p +N6ARISuSKpJEm2xTN7idluJ9yjmLlYtg6LbhKmXUQhGniz3M999DrQERTLDAF80h +tpbjVcWMnPsrX4AnQBFVEjs5zCHU1hD+X463EmUHBWyT975jbZ8Fy7/fTzkdzLnn +qE5lROALr2MCAAwQRFbRE6dd52vnXaBrVcAtRzjATts3WG3+SNi2Fm/OrYqQcY9e +lBexNviT8VcldOAMrO10E2u0d+tvxFzwB3ABMvaVamrEZky4XSfB6aLzpD0JJj1s +UHnIiVwJAoGBAPl8nLll/J9rud/N2HiAX2YkP0MC0HW4yM3KxLtXKyXrP5qBpaci +wTDUmSWEEE3GUJMM1Z4d9tl9Lz2MhU2KqkEvLI3kQ7aUu33PYUBGMVcUzhFQ49lU +Nzz8YB183iqo31o/DKk2Cr5gI7SykQZ0gn/urZkEJeErLzlhPXcyeY5jAoGBAOx4 +CGucVdv5MbdXZP8jVzxuvUlSp7BIQJ2phQXDFBNApFKnZn7yBYBx7dqzleymGm+R +INZAurg3SNw4nvbQc3Z2dJ8I+n5ErjFCKp1IedVxx1eMEfecTwrQZuUwLISIyjqF +czSJNwcNqzCx67z397/Cg5K/0pu6uIe0r7xozcbvAoGBAOOvZ9CDVPOg+rdXQvFm +Jqou9lUPonNtOkUlgjl+qfAnK5q0KxvHSgxoWYO1bLOuAybQlbuBmSCPcKd5MMa9 +f/eRN9YetfVQ83Mz6YshBDJ22EFRUz+p7eeIY6dFp/PCvmO8Gq/qlA996dglBtmf +RuG+T0vQT0mZgbWaGuBHfkwFAoGBAMOLg1MRxgKRMKavk6pU3EfyP3+J5XemWCDI +1WLtbgV5uClNmzmxBBGypQHs7jbzKPtHpULn5kB+HzdVb0clG8ZDsK7u6s5OF0pO +sBS+oVl7rF/eSeFcFhUYP26ZhsbWo3z/bERuj926VO2AxDPRTsP5o3pQPGZhY0V9 +irGgbUJrAoGBAOseS3J4BqYM4R3Hr7cRAhvzSjIkeTcDF1zTOa4FZDHBxZ6g2PNq +8ekhtfn1zPczsPTF1vNuqEISKLxaPkVPiw0mtaZQjVwpF/IOxMNjWVLp6oJf8Mm2 +BxlXqPnQ4mG66oqSFQgDEmFdMhRb2of6xL1gYYL62C80G2T7QtmPfSab +-----END RSA PRIVATE KEY----- +"; + + static Identity CreateRegisteredIdentity() + { + var identity = new Identity(Localization.Get("us")); + identity.Update( + new PrivateKey(SamplePrivateKey), + new AdpToken(SampleAdpToken), + new AccessToken(SampleAccessToken, SampleExpires), + new RefreshToken(SampleRefreshToken), + new List> { new("session-id", "cookie-secret-value") }, + deviceSerialNumber: "device-serial", + deviceType: "device-type", + amazonAccountId: "amzn-account", + deviceName: "device-name", + storeAuthenticationCookie: SampleStoreAuthCookie); + return identity; + } + + [TestMethod] + public void toJson_registered_account_has_no_IsEncrypted_and_plaintext_secrets() + { + var settings = new AccountsSettings(); + settings.Add(new Account("user@example.com") + { + AccountName = "Main", + IdentityTokens = CreateRegisteredIdentity() + }); + + var json = settings.ToJson(); + var jo = JObject.Parse(json); + + jo.SelectTokens("$..IsEncrypted").Should().HaveCount(0); + var tokens = jo["Accounts"]![0]!["IdentityTokens"]!; + tokens["ExistingAccessToken"]!["TokenValue"]!.Value().Should().Be(SampleAccessToken); + tokens["RefreshToken"]!["Value"]!.Value().Should().Be(SampleRefreshToken); + tokens["AdpToken"]!["Value"]!.Value().Should().Be(SampleAdpToken); + tokens["PrivateKey"]!["Value"]!.Value().Should().Be(SamplePrivateKey); + tokens["StoreAuthenticationCookie"]!.Value().Should().Be(SampleStoreAuthCookie); + tokens["Cookies"]![0]!["Value"]!.Value().Should().Be("cookie-secret-value"); + } + + [TestMethod] + public void fromJson_legacy_file_loads_usable_plaintext_tokens() + { + var settings = new AccountsSettings(); + settings.Add(new Account("user@example.com") + { + AccountName = "Main", + DecryptKey = "activation-bytes-ok-as-plaintext", + IdentityTokens = CreateRegisteredIdentity() + }); + var legacyJson = settings.ToJson(); + JObject.Parse(legacyJson).SelectTokens("$..IsEncrypted").Should().HaveCount(0); + + var loaded = AccountsSettings.FromJson(legacyJson); + loaded.BeNotNull(); + loaded.Accounts.Count.Should().Be(1); + + var account = loaded.Accounts[0]; + account.AccountId.Should().Be("user@example.com"); + account.DecryptKey.Should().Be("activation-bytes-ok-as-plaintext"); + account.IdentityTokens.BeNotNull(); + account.IdentityTokens.IsValid.Should().BeTrue(); + account.IdentityTokens.ExistingAccessToken.TokenValue.Should().Be(SampleAccessToken); + account.IdentityTokens.RefreshToken.BeNotNull(); + account.IdentityTokens.RefreshToken.Value.Should().Be(SampleRefreshToken); + account.IdentityTokens.AdpToken.BeNotNull(); + account.IdentityTokens.AdpToken.Value.Should().Be(SampleAdpToken); + account.IdentityTokens.PrivateKey.BeNotNull(); + account.IdentityTokens.PrivateKey.Value.Should().Be(SamplePrivateKey); + account.IdentityTokens.StoreAuthenticationCookie.Should().Be(SampleStoreAuthCookie); + account.IdentityTokens.Cookies.Single().Value.Should().Be("cookie-secret-value"); + } + + [TestMethod] + public void persister_roundtrip_keeps_plaintext_tokens_without_IsEncrypted() + { + using (var p = new AccountsSettingsPersister(new AccountsSettings(), TestFile)) + { + p.AccountsSettings.Add(new Account("user@example.com") + { + AccountName = "Main", + IdentityTokens = CreateRegisteredIdentity() + }); + } + + var onDisk = File.ReadAllText(TestFile); + var jo = JObject.Parse(onDisk); + jo.SelectTokens("$..IsEncrypted").Should().HaveCount(0); + jo["Accounts"]![0]!["IdentityTokens"]!["RefreshToken"]!["Value"]!.Value() + .Should().Be(SampleRefreshToken); + + using var loaded = new AccountsSettingsPersister(TestFile); + var tokens = loaded.AccountsSettings.Accounts[0].IdentityTokens; + tokens.BeNotNull(); + tokens.IsValid.Should().BeTrue(); + tokens.ExistingAccessToken.TokenValue.Should().Be(SampleAccessToken); + tokens.RefreshToken.BeNotNull(); + tokens.RefreshToken.Value.Should().Be(SampleRefreshToken); + } +} #pragma warning restore CS8981