diff --git a/CHANGES.md b/CHANGES.md index dc2cf223915..86777df0bcd 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -36,11 +36,22 @@ To be released. ### Added interfaces + - Added `ProtectedPrivateKey` class. [[#577], [#614]] + - Added `IncorrectPassphraseException` class. [[#577], [#614]] + - Added `MismatchedAddressException` class. [[#577], [#614]] + - Added `KeyJsonException` abstract class. [[#577], [#614]] + - Added `InvalidKeyJsonException` class. [[#577], [#614]] + - Added `UnsupportedKeyJsonException` class. [[#577], [#614]] + - Added `ICipher` interface. [[#577], [#614]] + - Added `Aes128Ctr` class. [[#577], [#614]] + - Added `IKdf` interface. [[#577], [#614]] + - Added `Pbkdf2` class. [[#577], [#614]] - Added `BlockChain.LongCount()` method. [[#575]] - Added `BlockChain[HashDigest]` indexer. [[#409], [#583]] - Added `BlockChain.Contains(HashDigest)` method. [[#409], [#583]] - Added `BlockChain.GetTransaction(TxId)` method. [[#409], [#583]] - Added `BlockChain.Contains(TxId)` method. [[#409], [#583]] + - Added `ByteUtil.Hex(ImmutableArray)` overloaded method. [[#614]] ### Behavioral changes @@ -112,6 +123,7 @@ To be released. [#568]: https://github.com/planetarium/libplanet/issues/568 [#575]: https://github.com/planetarium/libplanet/pull/575 [#576]: https://github.com/planetarium/libplanet/pull/576 +[#577]: https://github.com/planetarium/libplanet/issues/577 [#579]: https://github.com/planetarium/libplanet/pull/579 [#581]: https://github.com/planetarium/libplanet/pull/581 [#583]: https://github.com/planetarium/libplanet/pull/583 @@ -124,6 +136,7 @@ To be released. [#608]: https://github.com/planetarium/libplanet/pull/608 [#609]: https://github.com/planetarium/libplanet/pull/609 [#610]: https://github.com/planetarium/libplanet/pull/610 +[#614]: https://github.com/planetarium/libplanet/pull/614 [#622]: https://github.com/planetarium/libplanet/pull/622 diff --git a/Libplanet.Tests/ByteUtilTest.cs b/Libplanet.Tests/ByteUtilTest.cs index 980bbaa2c82..08dacdd8cf0 100644 --- a/Libplanet.Tests/ByteUtilTest.cs +++ b/Libplanet.Tests/ByteUtilTest.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Immutable; using Xunit; namespace Libplanet.Tests @@ -8,14 +9,14 @@ public class ByteUtilTest [Fact] public void HexTest() { - var bs = new byte[20] + var bs = new byte[] { 0x45, 0xa2, 0x21, 0x87, 0xe2, 0xd8, 0x85, 0x0b, 0xb3, 0x57, 0x88, 0x69, 0x58, 0xbc, 0x3e, 0x85, 0x60, 0x92, 0x9c, 0xcc, }; - Assert.Equal( - "45a22187e2d8850bb357886958bc3e8560929ccc", - ByteUtil.Hex(bs)); + const string expectedHex = "45a22187e2d8850bb357886958bc3e8560929ccc"; + Assert.Equal(expectedHex, ByteUtil.Hex(bs)); + Assert.Equal(expectedHex, ByteUtil.Hex(ImmutableArray.Create(bs))); } [Fact] diff --git a/Libplanet.Tests/KeyStore/Ciphers/Aes128CtrTest.cs b/Libplanet.Tests/KeyStore/Ciphers/Aes128CtrTest.cs new file mode 100644 index 00000000000..f31dca939de --- /dev/null +++ b/Libplanet.Tests/KeyStore/Ciphers/Aes128CtrTest.cs @@ -0,0 +1,97 @@ +using System; +using System.Collections.Immutable; +using System.Text.Json; +using Libplanet.KeyStore; +using Libplanet.KeyStore.Ciphers; +using Xunit; + +namespace Libplanet.Tests.KeyStore.Ciphers +{ + public class Aes128CtrTest : CipherTest + { + public Aes128CtrTest() + { + var random = new Random(); + var buffer = new byte[16]; + random.NextBytes(buffer); + Cipher = new Aes128Ctr(buffer.ToImmutableArray()); + } + + public override Aes128Ctr Cipher { get; } + + [Fact] + public void Constructor() + { + Assert.Throws(() => + new Aes128Ctr(new byte[0].ToImmutableArray()) + ); + var random = new Random(); + var buffer = new byte[17]; + random.NextBytes(buffer); + Assert.Throws(() => + new Aes128Ctr(buffer.ToImmutableArray()) + ); + } + + [Fact] + public void FromJson() + { + var options = new JsonDocumentOptions + { + AllowTrailingCommas = true, + CommentHandling = JsonCommentHandling.Skip, + }; + + Aes128Ctr Load(string json) + { + using (JsonDocument doc = JsonDocument.Parse(json, options)) + { + return (Aes128Ctr)Aes128Ctr.FromJson(doc.RootElement); + } + } + + Aes128Ctr cipher = Load(@"{ + ""iv"": ""bc7f2ca23bfee0dd9725228ab2b0d98a"", + }"); + TestUtils.AssertBytesEqual( + new byte[] + { + 0xbc, 0x7f, 0x2c, 0xa2, 0x3b, 0xfe, 0xe0, 0xdd, + 0x97, 0x25, 0x22, 0x8a, 0xb2, 0xb0, 0xd9, 0x8a, + }.ToImmutableArray(), + cipher.Iv + ); + + Assert.Throws(() => + Load(@"{ + // ""iv"": ""..."", // lacks + }") + ); + + Assert.Throws(() => + Load(@"{ + ""iv"": true, // not a string + }") + ); + + Assert.Throws(() => + Load(@"{ + ""iv"": null, // not a string, but null + }") + ); + + Assert.Throws(() => + Load(@"{ + ""iv"": ""not a hexadecimal string"", + }") + ); + + Assert.Throws(() => + Load(@"{ + ""iv"": ""bc7f2ca23bfee0dd9725228ab2b0d98"", + // iv: invalid length + }") + ); + } + } +} diff --git a/Libplanet.Tests/KeyStore/Ciphers/CipherTest.cs b/Libplanet.Tests/KeyStore/Ciphers/CipherTest.cs new file mode 100644 index 00000000000..db5dae60ad4 --- /dev/null +++ b/Libplanet.Tests/KeyStore/Ciphers/CipherTest.cs @@ -0,0 +1,28 @@ +using System.Collections.Immutable; +using Libplanet.KeyStore.Ciphers; +using Xunit; +using Random = System.Random; + +namespace Libplanet.Tests.KeyStore.Ciphers +{ + public abstract class CipherTest + where T : ICipher + { + public abstract T Cipher { get; } + + [Fact] + public void EncryptDecrypt() + { + var random = new Random(); + var buffer = new byte[4096]; + random.NextBytes(buffer); + ImmutableArray key = ImmutableArray.Create(buffer, 0, 16); + random.NextBytes(buffer); + ImmutableArray value = buffer.ToImmutableArray(); + T c = Cipher; + ImmutableArray encrypted = c.Encrypt(key, value); + ImmutableArray decrypted = c.Decrypt(key, encrypted); + TestUtils.AssertBytesEqual(value, decrypted); + } + } +} diff --git a/Libplanet.Tests/KeyStore/IncorrectPassphraseExceptionTest.cs b/Libplanet.Tests/KeyStore/IncorrectPassphraseExceptionTest.cs new file mode 100644 index 00000000000..36471cf6ac7 --- /dev/null +++ b/Libplanet.Tests/KeyStore/IncorrectPassphraseExceptionTest.cs @@ -0,0 +1,31 @@ +using System.Collections.Immutable; +using Libplanet.KeyStore; +using Xunit; +using static Libplanet.Tests.TestUtils; + +namespace Libplanet.Tests.KeyStore +{ + public class IncorrectPassphraseExceptionTest + { + [Fact] + public void Constructor() + { + ImmutableArray + expectedMac = new byte[] { 0x00, 0x01, 0x02, 0x03 }.ToImmutableArray(), + inputMac = new byte[] { 0x04, 0x05, 0x06, 0x07 }.ToImmutableArray(); + var e = new IncorrectPassphraseException( + "Some message.", + "paramName", + expectedMac, + inputMac + ); + Assert.StartsWith( + "Some message.\nExpected MAC: 00010203\nInput MAC: 04050607", + e.Message + ); + Assert.Equal("paramName", e.ParamName); + AssertBytesEqual(expectedMac, e.ExpectedMac); + AssertBytesEqual(inputMac, e.InputMac); + } + } +} diff --git a/Libplanet.Tests/KeyStore/Kdfs/KdfTest.cs b/Libplanet.Tests/KeyStore/Kdfs/KdfTest.cs new file mode 100644 index 00000000000..f1639332c24 --- /dev/null +++ b/Libplanet.Tests/KeyStore/Kdfs/KdfTest.cs @@ -0,0 +1,32 @@ +using System.Collections.Immutable; +using System.Security.Cryptography; +using Libplanet.KeyStore.Kdfs; +using Xunit; + +namespace Libplanet.Tests.KeyStore.Kdfs +{ + public abstract class KdfTest + where T : IKdf + { + public abstract T MakeInstance(byte[] randomBytes); + + [InlineData(16)] + [InlineData(32)] + [Theory] + public void Derive(int size) + { + var randomBytes = new byte[size]; + using (RandomNumberGenerator rng = RandomNumberGenerator.Create()) + { + rng.GetBytes(randomBytes); + } + + T kdf = MakeInstance(randomBytes); + ImmutableArray dFoo = kdf.Derive("foo"); + Assert.Equal(size, dFoo.Length); + ImmutableArray dBar = kdf.Derive("bar"); + Assert.NotEqual(dFoo, dBar); + TestUtils.AssertBytesEqual(dFoo, kdf.Derive("foo")); + } + } +} diff --git a/Libplanet.Tests/KeyStore/Kdfs/Pbkdf2Test.cs b/Libplanet.Tests/KeyStore/Kdfs/Pbkdf2Test.cs new file mode 100644 index 00000000000..9ec786675d8 --- /dev/null +++ b/Libplanet.Tests/KeyStore/Kdfs/Pbkdf2Test.cs @@ -0,0 +1,199 @@ +using System.Collections.Immutable; +using System.Text.Json; +using Libplanet.KeyStore; +using Libplanet.KeyStore.Kdfs; +using Org.BouncyCastle.Crypto.Digests; +using Xunit; + +namespace Libplanet.Tests.KeyStore.Kdfs +{ + public class Pbkdf2Test : KdfTest> + { + public override Pbkdf2 MakeInstance(byte[] randomBytes) + { + return new Pbkdf2(10, randomBytes, randomBytes.Length); + } + + [Fact] + public void FromJson() + { + var options = new JsonDocumentOptions + { + AllowTrailingCommas = true, + CommentHandling = JsonCommentHandling.Skip, + }; + + Pbkdf2 Load(string json) + { + using (JsonDocument doc = JsonDocument.Parse(json, options)) + { + return (Pbkdf2)Pbkdf2.FromJson(doc.RootElement); + } + } + + var kdf = Load(@" + { + ""c"": 10240, + ""dklen"": 32, + ""prf"": ""hmac-sha256"", + ""salt"": ""3eeaaf35da70928387cae1ead31ed782b1135d7578a89d95e30cc914010ba2ed"", + } + "); + Assert.Equal(10240, kdf.Iterations); + Assert.Equal(32, kdf.KeyLength); + TestUtils.AssertBytesEqual( + new byte[] + { + 0x3e, 0xea, 0xaf, 0x35, 0xda, 0x70, 0x92, 0x83, 0x87, 0xca, 0xe1, + 0xea, 0xd3, 0x1e, 0xd7, 0x82, 0xb1, 0x13, 0x5d, 0x75, 0x78, 0xa8, + 0x9d, 0x95, 0xe3, 0x0c, 0xc9, 0x14, 0x01, 0x0b, 0xa2, 0xed, + }.ToImmutableArray(), + kdf.Salt + ); + + Assert.Throws(() => + Load(@" + { + // ""c"": 10240, // lacks + ""dklen"": 32, + ""prf"": ""hmac-sha256"", + ""salt"": ""3eeaaf35da70928387cae1ead31ed782b1135d7578a89d95e30cc914010ba2ed"", + } + ") + ); + + Assert.Throws(() => + Load(@" + { + ""c"": true, // not a number + ""dklen"": 32, + ""prf"": ""hmac-sha256"", + ""salt"": ""3eeaaf35da70928387cae1ead31ed782b1135d7578a89d95e30cc914010ba2ed"", + } + ") + ); + + Assert.Throws(() => + Load(@" + { + ""c"": null, // not a number, but null + ""dklen"": 32, + ""prf"": ""hmac-sha256"", + ""salt"": ""3eeaaf35da70928387cae1ead31ed782b1135d7578a89d95e30cc914010ba2ed"", + } + ") + ); + + Assert.Throws(() => + Load(@" + { + ""c"": 10240, + // ""dklen"": 32, // lacks + ""prf"": ""hmac-sha256"", + ""salt"": ""3eeaaf35da70928387cae1ead31ed782b1135d7578a89d95e30cc914010ba2ed"", + } + ") + ); + + Assert.Throws(() => + Load(@" + { + ""c"": 10240, + ""dklen"": false, // not a number + ""prf"": ""hmac-sha256"", + ""salt"": ""3eeaaf35da70928387cae1ead31ed782b1135d7578a89d95e30cc914010ba2ed"", + } + ") + ); + + Assert.Throws(() => + Load(@" + { + ""c"": 10240, + ""dklen"": null, // not a number, but null + ""prf"": ""hmac-sha256"", + ""salt"": ""3eeaaf35da70928387cae1ead31ed782b1135d7578a89d95e30cc914010ba2ed"", + } + ") + ); + + Assert.Throws(() => + Load(@" + { + ""c"": 10240, + ""dklen"": 32, + // ""prf"": ""hmac-sha256"", // lacks + ""salt"": ""3eeaaf35da70928387cae1ead31ed782b1135d7578a89d95e30cc914010ba2ed"", + } + ") + ); + + Assert.Throws(() => + Load(@" + { + ""c"": 10240, + ""dklen"": 32, + ""prf"": 123, // not a string, but a number + ""salt"": ""3eeaaf35da70928387cae1ead31ed782b1135d7578a89d95e30cc914010ba2ed"", + } + ") + ); + + Assert.Throws(() => + Load(@" + { + ""c"": 10240, + ""dklen"": 32, + ""prf"": ""hmac-sha512"", // unsupported prf + ""salt"": ""3eeaaf35da70928387cae1ead31ed782b1135d7578a89d95e30cc914010ba2ed"", + } + ") + ); + + Assert.Throws(() => + Load(@" + { + ""c"": 10240, + ""dklen"": 32, + ""prf"": ""hmac-sha256"", + // ""salt"": ""..."", // lacks + } + ") + ); + + Assert.Throws(() => + Load(@" + { + ""c"": 10240, + ""dklen"": 32, + ""prf"": ""hmac-sha256"", + ""salt"": 1234, // not a string, but a number + } + ") + ); + + Assert.Throws(() => + Load(@" + { + ""c"": 10240, + ""dklen"": 32, + ""prf"": ""hmac-sha256"", + ""salt"": ""not a hexadecimal string"", + } + ") + ); + + Assert.Throws(() => + Load(@" + { + ""c"": 10240, + ""dklen"": 32, + ""prf"": ""hmac-sha256"", + ""salt"": ""3eeaaf35da70928387cae1ead31ed782b1135d7578a89d95e30cc914010ba2e"", + // salt: invalid length + } + ") + ); + } + } +} diff --git a/Libplanet.Tests/KeyStore/MismatchedAddressExceptionTest.cs b/Libplanet.Tests/KeyStore/MismatchedAddressExceptionTest.cs new file mode 100644 index 00000000000..4eb33d784f4 --- /dev/null +++ b/Libplanet.Tests/KeyStore/MismatchedAddressExceptionTest.cs @@ -0,0 +1,28 @@ +using Libplanet.KeyStore; +using Xunit; + +namespace Libplanet.Tests.KeyStore +{ + public class MismatchedAddressExceptionTest + { + [Fact] + public void Constructor() + { + Address + expectedAddress = default, + actualAddress = new Address("01234567789aBcdEF01234567789ABCdeF012345"); + var e = new MismatchedAddressException( + "Some message.", + expectedAddress, + actualAddress + ); + Assert.Equal( + "Some message.\nExpected address: 0x0000000000000000000000000000000000000000\n" + + "Actual address: 0x01234567789aBcdEF01234567789ABCdeF012345", + e.Message + ); + Assert.Equal(expectedAddress, e.ExpectedAddress); + Assert.Equal(actualAddress, e.ActualAddress); + } + } +} diff --git a/Libplanet.Tests/KeyStore/ProtectedPrivateKeyTest.cs b/Libplanet.Tests/KeyStore/ProtectedPrivateKeyTest.cs new file mode 100644 index 00000000000..acdb1cfa7d8 --- /dev/null +++ b/Libplanet.Tests/KeyStore/ProtectedPrivateKeyTest.cs @@ -0,0 +1,699 @@ +using System; +using System.Collections.Immutable; +using System.IO; +using System.Text; +using Libplanet.Crypto; +using Libplanet.KeyStore; +using Libplanet.KeyStore.Ciphers; +using Libplanet.KeyStore.Kdfs; +using Org.BouncyCastle.Crypto.Digests; +using Xunit; +using static Libplanet.Tests.TestUtils; + +namespace Libplanet.Tests.KeyStore +{ + public class ProtectedPrivateKeyTest + { + public const string PassphraseFixture = "asdf"; + + public static readonly byte[] CiphertextFixture = + { + 0x53, 0x5b, 0xab, 0x75, 0xc1, 0x12, 0xfe, 0x32, 0x7d, 0xc2, 0xe2, + 0x93, 0xf8, 0x02, 0x97, 0xff, 0x33, 0x9e, 0x1e, 0x3c, 0xb7, 0x67, + 0x49, 0x61, 0x53, 0x13, 0xcd, 0xc2, 0xaa, 0xa3, 0xb3, 0x7a, + }; + + public static readonly ImmutableArray IvFixture = new byte[] + { + 0xbc, 0x7f, 0x2c, 0xa2, 0x3b, 0xfe, 0xe0, 0xdd, + 0x97, 0x25, 0x22, 0x8a, 0xb2, 0xb0, 0xd9, 0x8a, + }.ToImmutableArray(); + + public static readonly byte[] SaltFixture = + { + 0x3e, 0xea, 0xaf, 0x35, 0xda, 0x70, 0x92, 0x83, 0x87, 0xca, 0xe1, + 0xea, 0xd3, 0x1e, 0xd7, 0x82, 0xb1, 0x13, 0x5d, 0x75, 0x78, 0xa8, + 0x9d, 0x95, 0xe3, 0x0c, 0xc9, 0x14, 0x01, 0x0b, 0xa2, 0xed, + }; + + public static readonly byte[] MacFixture = + { + 0xd8, 0x6a, 0xb9, 0xef, 0xf2, 0x3f, 0x28, 0x21, 0x0d, 0xc0, 0x10, + 0x2a, 0x23, 0x64, 0x98, 0xe5, 0xc9, 0x68, 0x88, 0xbe, 0x6b, 0x5c, + 0xd6, 0xf3, 0x09, 0x81, 0xaa, 0x89, 0x6b, 0xe8, 0x42, 0xd1, + }; + + public static readonly Address AddressFixture = + new Address("d80d933db45cc0cf69e9632090f8aaff635dc8e5"); + + public static readonly IKdf KdfFixture = new Pbkdf2(10240, SaltFixture, 32); + + public static readonly ICipher CipherFixture = new Aes128Ctr(IvFixture); + + public static readonly ProtectedPrivateKey Fixture = new ProtectedPrivateKey( + AddressFixture, + KdfFixture, + MacFixture, + CipherFixture, + CiphertextFixture + ); + + [Fact] + public void Unprotect() + { + Assert.Equal(AddressFixture, Fixture.Address); + Assert.Equal( + AddressFixture, + Fixture.Unprotect(PassphraseFixture).PublicKey.ToAddress() + ); + var incorrectPassphraseException = Assert.Throws( + () => Fixture.Unprotect("wrong passphrase") + ); + TestUtils.AssertBytesEqual( + MacFixture.ToImmutableArray(), + incorrectPassphraseException.ExpectedMac + ); + Assert.NotEqual(MacFixture, incorrectPassphraseException.InputMac); + + var invalidPpk = new ProtectedPrivateKey( + default, + KdfFixture, + MacFixture, + CipherFixture, + CiphertextFixture + ); + var mismatchedAddressException = Assert.Throws( + () => invalidPpk.Unprotect(PassphraseFixture) + ); + Assert.Equal(default(Address), mismatchedAddressException.ExpectedAddress); + Assert.Equal(AddressFixture, mismatchedAddressException.ActualAddress); + } + + [Fact] + public void Protect() + { + PrivateKey privKey = new PrivateKey(); + ProtectedPrivateKey protectedKey = ProtectedPrivateKey.Protect(privKey, "foobar"); + AssertBytesEqual( + privKey.ByteArray, + protectedKey.Unprotect("foobar").ByteArray + ); + } + + [Fact] + public void FromJson() + { + ProtectedPrivateKey key = ProtectedPrivateKey.FromJson(@"{ + ""address"": ""d80d933db45cc0cf69e9632090f8aaff635dc8e5"", + ""crypto"": { + ""cipher"": ""aes-128-ctr"", + ""cipherparams"": { + ""iv"": ""bc7f2ca23bfee0dd9725228ab2b0d98a"", + }, + ""ciphertext"": + ""535bab75c112fe327dc2e293f80297ff339e1e3cb76749615313cdc2aaa3b37a"", + ""kdf"": ""pbkdf2"", + ""kdfparams"": { + ""c"": 10240, + ""dklen"": 32, + ""prf"": ""hmac-sha256"", + ""salt"": + ""3eeaaf35da70928387cae1ead31ed782b1135d7578a89d95e30cc914010ba2ed"", + }, + ""mac"": ""d86ab9eff23f28210dc0102a236498e5c96888be6b5cd6f30981aa896be842d1"", + }, + ""id"": ""3b647634-1234-cd02-e93c-02e1c0f54faa"", + ""version"": 3, + }"); + + Assert.Equal( + new Address("d80d933db45cc0cf69e9632090f8aaff635dc8e5"), + key.Address + ); + Assert.IsType(key.Cipher); + AssertBytesEqual( + new byte[] + { + 0x53, 0x5b, 0xab, 0x75, 0xc1, 0x12, 0xfe, 0x32, 0x7d, 0xc2, 0xe2, + 0x93, 0xf8, 0x02, 0x97, 0xff, 0x33, 0x9e, 0x1e, 0x3c, 0xb7, 0x67, + 0x49, 0x61, 0x53, 0x13, 0xcd, 0xc2, 0xaa, 0xa3, 0xb3, 0x7a, + }.ToImmutableArray(), + key.Ciphertext + ); + Assert.IsType>(key.Kdf); + AssertBytesEqual( + new byte[] + { + 0xd8, 0x6a, 0xb9, 0xef, 0xf2, 0x3f, 0x28, 0x21, 0x0d, 0xc0, 0x10, + 0x2a, 0x23, 0x64, 0x98, 0xe5, 0xc9, 0x68, 0x88, 0xbe, 0x6b, 0x5c, + 0xd6, 0xf3, 0x09, 0x81, 0xaa, 0x89, 0x6b, 0xe8, 0x42, 0xd1, + }.ToImmutableArray(), + key.Mac + ); + } + + #pragma warning disable MEN003 + [Fact] + public void FromJsonInvalidCases() + { + Func load = ProtectedPrivateKey.FromJson; + + Assert.Throws(() => + load("[] // Not an object") + ); + + Assert.Throws(() => + load(@"{ + ""address"": ""d80d933db45cc0cf69e9632090f8aaff635dc8e5"", + ""id"": ""3b647634-1234-cd02-e93c-02e1c0f54faa"", + // ""version"": 3, // Lacks + }") + ); + + Assert.Throws(() => + load(@"{ + ""address"": ""d80d933db45cc0cf69e9632090f8aaff635dc8e5"", + ""id"": ""3b647634-1234-cd02-e93c-02e1c0f54faa"", + ""version"": true, // Not a number + }") + ); + + Assert.Throws(() => + load(@"{ + ""address"": ""d80d933db45cc0cf69e9632090f8aaff635dc8e5"", + ""id"": ""3b647634-1234-cd02-e93c-02e1c0f54faa"", + ""version"": 2, // Unsupported version + }") + ); + + Assert.Throws(() => + load(@"{ + ""address"": ""d80d933db45cc0cf69e9632090f8aaff635dc8e5"", + // ""crypto"": {}, // Lacks + ""id"": ""3b647634-1234-cd02-e93c-02e1c0f54faa"", + ""version"": 3, + }") + ); + + Assert.Throws(() => + load(@"{ + ""address"": ""d80d933db45cc0cf69e9632090f8aaff635dc8e5"", + ""crypto"": null, // Not an object + ""id"": ""3b647634-1234-cd02-e93c-02e1c0f54faa"", + ""version"": 3, + }") + ); + + Assert.Throws(() => + load(@"{ + // ""address"": ""d80d933db45cc0cf69e9632090f8aaff635dc8e5"", // Lacks + ""crypto"": { + ""cipher"": ""aes-128-ctr"", + ""cipherparams"": { + ""iv"": ""bc7f2ca23bfee0dd9725228ab2b0d98a"", + }, + ""ciphertext"": + ""535bab75c112fe327dc2e293f80297ff339e1e3cb76749615313cdc2aaa3b37a"", + ""kdf"": ""pbkdf2"", + ""kdfparams"": { + ""c"": 10240, + ""dklen"": 32, + ""prf"": ""hmac-sha256"", + ""salt"": + ""3eeaaf35da70928387cae1ead31ed782b1135d7578a89d95e30cc914010ba2ed"" + }, + ""mac"": + ""d86ab9eff23f28210dc0102a236498e5c96888be6b5cd6f30981aa896be842d1"", + }, + ""id"": ""3b647634-1234-cd02-e93c-02e1c0f54faa"", + ""version"": 3, + }") + ); + + Assert.Throws(() => + load(@"{ + ""address"": 123, // Not a string + ""crypto"": { + ""cipher"": ""aes-128-ctr"", + ""cipherparams"": { + ""iv"": ""bc7f2ca23bfee0dd9725228ab2b0d98a"", + }, + ""ciphertext"": + ""535bab75c112fe327dc2e293f80297ff339e1e3cb76749615313cdc2aaa3b37a"", + ""kdf"": ""pbkdf2"", + ""kdfparams"": { + ""c"": 10240, + ""dklen"": 32, + ""prf"": ""hmac-sha256"", + ""salt"": + ""3eeaaf35da70928387cae1ead31ed782b1135d7578a89d95e30cc914010ba2ed"" + }, + ""mac"": + ""d86ab9eff23f28210dc0102a236498e5c96888be6b5cd6f30981aa896be842d1"", + }, + ""id"": ""3b647634-1234-cd02-e93c-02e1c0f54faa"", + ""version"": 3, + }") + ); + + Assert.Throws(() => + load(@"{ + ""address"": null, // Not a string, but null + ""crypto"": { + ""cipher"": ""aes-128-ctr"", + ""cipherparams"": { + ""iv"": ""bc7f2ca23bfee0dd9725228ab2b0d98a"", + }, + ""ciphertext"": + ""535bab75c112fe327dc2e293f80297ff339e1e3cb76749615313cdc2aaa3b37a"", + ""kdf"": ""pbkdf2"", + ""kdfparams"": { + ""c"": 10240, + ""dklen"": 32, + ""prf"": ""hmac-sha256"", + ""salt"": + ""3eeaaf35da70928387cae1ead31ed782b1135d7578a89d95e30cc914010ba2ed"" + }, + ""mac"": + ""d86ab9eff23f28210dc0102a236498e5c96888be6b5cd6f30981aa896be842d1"", + }, + ""id"": ""3b647634-1234-cd02-e93c-02e1c0f54faa"", + ""version"": 3, + }") + ); + + Assert.Throws(() => + load(@"{ + ""address"": ""Not a hexadecimal string"", + ""crypto"": { + ""cipher"": ""aes-128-ctr"", + ""cipherparams"": { + ""iv"": ""bc7f2ca23bfee0dd9725228ab2b0d98a"", + }, + ""ciphertext"": + ""535bab75c112fe327dc2e293f80297ff339e1e3cb76749615313cdc2aaa3b37a"", + ""kdf"": ""pbkdf2"", + ""kdfparams"": { + ""c"": 10240, + ""dklen"": 32, + ""prf"": ""hmac-sha256"", + ""salt"": + ""3eeaaf35da70928387cae1ead31ed782b1135d7578a89d95e30cc914010ba2ed"" + }, + ""mac"": + ""d86ab9eff23f28210dc0102a236498e5c96888be6b5cd6f30981aa896be842d1"", + }, + ""id"": ""3b647634-1234-cd02-e93c-02e1c0f54faa"", + ""version"": 3, + }") + ); + + Assert.Throws(() => + load(@"{ + ""address"": ""d80d933db45cc0cf69e9632090f8aaff635dc8e50"", // Invalid length + ""crypto"": { + ""cipher"": ""aes-128-ctr"", + ""cipherparams"": { + ""iv"": ""bc7f2ca23bfee0dd9725228ab2b0d98a"", + }, + ""ciphertext"": + ""535bab75c112fe327dc2e293f80297ff339e1e3cb76749615313cdc2aaa3b37a"", + ""kdf"": ""pbkdf2"", + ""kdfparams"": { + ""c"": 10240, + ""dklen"": 32, + ""prf"": ""hmac-sha256"", + ""salt"": + ""3eeaaf35da70928387cae1ead31ed782b1135d7578a89d95e30cc914010ba2ed"" + }, + ""mac"": + ""d86ab9eff23f28210dc0102a236498e5c96888be6b5cd6f30981aa896be842d1"", + }, + ""id"": ""3b647634-1234-cd02-e93c-02e1c0f54faa"", + ""version"": 3, + }") + ); + + Assert.Throws(() => + load(@"{ + ""address"": ""d80d933db45cc0cf69e9632090f8aaff635dc8e5"", + ""crypto"": { + // ""cipher"": ""aes-128-ctr"", // Lacks + ""cipherparams"": { + ""iv"": ""bc7f2ca23bfee0dd9725228ab2b0d98a"", + }, + ""ciphertext"": + ""535bab75c112fe327dc2e293f80297ff339e1e3cb76749615313cdc2aaa3b37a"", + ""kdf"": ""pbkdf2"", + ""kdfparams"": { + ""c"": 10240, + ""dklen"": 32, + ""prf"": ""hmac-sha256"", + ""salt"": + ""3eeaaf35da70928387cae1ead31ed782b1135d7578a89d95e30cc914010ba2ed"" + }, + ""mac"": + ""d86ab9eff23f28210dc0102a236498e5c96888be6b5cd6f30981aa896be842d1"", + }, + ""id"": ""3b647634-1234-cd02-e93c-02e1c0f54faa"", + ""version"": 3, + }") + ); + + Assert.Throws(() => + load(@"{ + ""address"": ""d80d933db45cc0cf69e9632090f8aaff635dc8e5"", + ""crypto"": { + ""cipher"": null, // Not a string, but null + ""cipherparams"": { + ""iv"": ""bc7f2ca23bfee0dd9725228ab2b0d98a"", + }, + ""ciphertext"": + ""535bab75c112fe327dc2e293f80297ff339e1e3cb76749615313cdc2aaa3b37a"", + ""kdf"": ""pbkdf2"", + ""kdfparams"": { + ""c"": 10240, + ""dklen"": 32, + ""prf"": ""hmac-sha256"", + ""salt"": + ""3eeaaf35da70928387cae1ead31ed782b1135d7578a89d95e30cc914010ba2ed"" + }, + ""mac"": + ""d86ab9eff23f28210dc0102a236498e5c96888be6b5cd6f30981aa896be842d1"", + }, + ""id"": ""3b647634-1234-cd02-e93c-02e1c0f54faa"", + ""version"": 3, + }") + ); + + Assert.Throws(() => + load(@"{ + ""address"": ""d80d933db45cc0cf69e9632090f8aaff635dc8e5"", + ""crypto"": { + ""cipher"": ""aes-256-ctr"", // Unsupported + ""cipherparams"": { + ""iv"": ""bc7f2ca23bfee0dd9725228ab2b0d98a"", + }, + ""ciphertext"": + ""535bab75c112fe327dc2e293f80297ff339e1e3cb76749615313cdc2aaa3b37a"", + ""kdf"": ""pbkdf2"", + ""kdfparams"": { + ""c"": 10240, + ""dklen"": 32, + ""prf"": ""hmac-sha256"", + ""salt"": + ""3eeaaf35da70928387cae1ead31ed782b1135d7578a89d95e30cc914010ba2ed"" + }, + ""mac"": + ""d86ab9eff23f28210dc0102a236498e5c96888be6b5cd6f30981aa896be842d1"", + }, + ""id"": ""3b647634-1234-cd02-e93c-02e1c0f54faa"", + ""version"": 3, + }") + ); + + Assert.Throws(() => + load(@"{ + ""address"": ""d80d933db45cc0cf69e9632090f8aaff635dc8e5"", + ""crypto"": { + ""cipher"": ""aes-128-ctr"", + // ""cipherparams"": {}, // Lacks + ""ciphertext"": + ""535bab75c112fe327dc2e293f80297ff339e1e3cb76749615313cdc2aaa3b37a"", + ""kdf"": ""pbkdf2"", + ""kdfparams"": { + ""c"": 10240, + ""dklen"": 32, + ""prf"": ""hmac-sha256"", + ""salt"": + ""3eeaaf35da70928387cae1ead31ed782b1135d7578a89d95e30cc914010ba2ed"" + }, + ""mac"": + ""d86ab9eff23f28210dc0102a236498e5c96888be6b5cd6f30981aa896be842d1"", + }, + ""id"": ""3b647634-1234-cd02-e93c-02e1c0f54faa"", + ""version"": 3, + }") + ); + + Assert.Throws(() => + load(@"{ + ""address"": ""d80d933db45cc0cf69e9632090f8aaff635dc8e5"", + ""crypto"": { + ""cipher"": ""aes-128-ctr"", + ""cipherparams"": ""Not an object, but a string"", + ""ciphertext"": + ""535bab75c112fe327dc2e293f80297ff339e1e3cb76749615313cdc2aaa3b37a"", + ""kdf"": ""pbkdf2"", + ""kdfparams"": { + ""c"": 10240, + ""dklen"": 32, + ""prf"": ""hmac-sha256"", + ""salt"": + ""3eeaaf35da70928387cae1ead31ed782b1135d7578a89d95e30cc914010ba2ed"" + }, + ""mac"": + ""d86ab9eff23f28210dc0102a236498e5c96888be6b5cd6f30981aa896be842d1"", + }, + ""id"": ""3b647634-1234-cd02-e93c-02e1c0f54faa"", + ""version"": 3, + }") + ); + + Assert.Throws(() => + load(@"{ + ""address"": ""d80d933db45cc0cf69e9632090f8aaff635dc8e5"", + ""crypto"": { + ""cipher"": ""aes-128-ctr"", + ""cipherparams"": { + ""iv"": ""bc7f2ca23bfee0dd9725228ab2b0d98a"", + }, + // ""ciphertext"": ""..."", // Lacks + ""kdf"": ""pbkdf2"", + ""kdfparams"": { + ""c"": 10240, + ""dklen"": 32, + ""prf"": ""hmac-sha256"", + ""salt"": + ""3eeaaf35da70928387cae1ead31ed782b1135d7578a89d95e30cc914010ba2ed"" + }, + ""mac"": + ""d86ab9eff23f28210dc0102a236498e5c96888be6b5cd6f30981aa896be842d1"", + }, + ""id"": ""3b647634-1234-cd02-e93c-02e1c0f54faa"", + ""version"": 3, + }") + ); + + Assert.Throws(() => + load(@"{ + ""address"": ""d80d933db45cc0cf69e9632090f8aaff635dc8e5"", + ""crypto"": { + ""cipher"": ""aes-128-ctr"", + ""cipherparams"": { + ""iv"": ""bc7f2ca23bfee0dd9725228ab2b0d98a"", + }, + ""ciphertext"": + ""535bab75c112fe327dc2e293f80297ff339e1e3cb76749615313cdc2aaa3b37a"", + // ""kdf"": ""pbkdf2"", // Lacks + ""kdfparams"": { + ""c"": 10240, + ""dklen"": 32, + ""prf"": ""hmac-sha256"", + ""salt"": + ""3eeaaf35da70928387cae1ead31ed782b1135d7578a89d95e30cc914010ba2ed"" + }, + ""mac"": + ""d86ab9eff23f28210dc0102a236498e5c96888be6b5cd6f30981aa896be842d1"", + }, + ""id"": ""3b647634-1234-cd02-e93c-02e1c0f54faa"", + ""version"": 3, + }") + ); + + Assert.Throws(() => + load(@"{ + ""address"": ""d80d933db45cc0cf69e9632090f8aaff635dc8e5"", + ""crypto"": { + ""cipher"": ""aes-128-ctr"", + ""cipherparams"": { + ""iv"": ""bc7f2ca23bfee0dd9725228ab2b0d98a"", + }, + ""ciphertext"": + ""535bab75c112fe327dc2e293f80297ff339e1e3cb76749615313cdc2aaa3b37a"", + ""kdf"": 123, // Not a string, but a number + ""kdfparams"": { + ""c"": 10240, + ""dklen"": 32, + ""prf"": ""hmac-sha256"", + ""salt"": + ""3eeaaf35da70928387cae1ead31ed782b1135d7578a89d95e30cc914010ba2ed"" + }, + ""mac"": + ""d86ab9eff23f28210dc0102a236498e5c96888be6b5cd6f30981aa896be842d1"", + }, + ""id"": ""3b647634-1234-cd02-e93c-02e1c0f54faa"", + ""version"": 3, + }") + ); + + Assert.Throws(() => + load(@"{ + ""address"": ""d80d933db45cc0cf69e9632090f8aaff635dc8e5"", + ""crypto"": { + ""cipher"": ""aes-128-ctr"", + ""cipherparams"": { + ""iv"": ""bc7f2ca23bfee0dd9725228ab2b0d98a"", + }, + ""ciphertext"": + ""535bab75c112fe327dc2e293f80297ff339e1e3cb76749615313cdc2aaa3b37a"", + ""kdf"": ""unsupported-kdf"", // Unsupported KDF + ""kdfparams"": { + ""c"": 10240, + ""dklen"": 32, + ""prf"": ""hmac-sha256"", + ""salt"": + ""3eeaaf35da70928387cae1ead31ed782b1135d7578a89d95e30cc914010ba2ed"" + }, + ""mac"": + ""d86ab9eff23f28210dc0102a236498e5c96888be6b5cd6f30981aa896be842d1"", + }, + ""id"": ""3b647634-1234-cd02-e93c-02e1c0f54faa"", + ""version"": 3, + }") + ); + + Assert.Throws(() => + load(@"{ + ""address"": ""d80d933db45cc0cf69e9632090f8aaff635dc8e5"", + ""crypto"": { + ""cipher"": ""aes-128-ctr"", + ""cipherparams"": { + ""iv"": ""bc7f2ca23bfee0dd9725228ab2b0d98a"", + }, + ""ciphertext"": + ""535bab75c112fe327dc2e293f80297ff339e1e3cb76749615313cdc2aaa3b37a"", + ""kdf"": ""pbkdf2"", + // ""kdfparams"": {}, // Lacks + ""mac"": + ""d86ab9eff23f28210dc0102a236498e5c96888be6b5cd6f30981aa896be842d1"", + }, + ""id"": ""3b647634-1234-cd02-e93c-02e1c0f54faa"", + ""version"": 3, + }") + ); + + Assert.Throws(() => + load(@"{ + ""address"": ""d80d933db45cc0cf69e9632090f8aaff635dc8e5"", + ""crypto"": { + ""cipher"": ""aes-128-ctr"", + ""cipherparams"": { + ""iv"": ""bc7f2ca23bfee0dd9725228ab2b0d98a"", + }, + ""ciphertext"": + ""535bab75c112fe327dc2e293f80297ff339e1e3cb76749615313cdc2aaa3b37a"", + ""kdf"": ""pbkdf2"", + ""kdfparams"": ""Not an object, but a string"", + ""mac"": + ""d86ab9eff23f28210dc0102a236498e5c96888be6b5cd6f30981aa896be842d1"", + }, + ""id"": ""3b647634-1234-cd02-e93c-02e1c0f54faa"", + ""version"": 3, + }") + ); + + Assert.Throws(() => + load(@"{ + ""address"": ""d80d933db45cc0cf69e9632090f8aaff635dc8e5"", + ""crypto"": { + ""cipher"": ""aes-128-ctr"", + ""cipherparams"": { + ""iv"": ""bc7f2ca23bfee0dd9725228ab2b0d98a"", + }, + ""ciphertext"": + ""535bab75c112fe327dc2e293f80297ff339e1e3cb76749615313cdc2aaa3b37a"", + ""kdf"": ""pbkdf2"", + ""kdfparams"": { + ""c"": 10240, + ""dklen"": 32, + ""prf"": ""hmac-sha256"", + ""salt"": + ""3eeaaf35da70928387cae1ead31ed782b1135d7578a89d95e30cc914010ba2ed"" + }, + // ""mac"": ""..."", // Lacks + }, + ""id"": ""3b647634-1234-cd02-e93c-02e1c0f54faa"", + ""version"": 3, + }") + ); + + Assert.Throws(() => + load(@"{ + ""address"": ""d80d933db45cc0cf69e9632090f8aaff635dc8e5"", + ""crypto"": { + ""cipher"": ""aes-128-ctr"", + ""cipherparams"": { + ""iv"": ""bc7f2ca23bfee0dd9725228ab2b0d98a"", + }, + ""ciphertext"": + ""535bab75c112fe327dc2e293f80297ff339e1e3cb76749615313cdc2aaa3b37a"", + ""kdf"": ""pbkdf2"", + ""kdfparams"": { + ""c"": 10240, + ""dklen"": 32, + ""prf"": ""hmac-sha256"", + ""salt"": + ""3eeaaf35da70928387cae1ead31ed782b1135d7578a89d95e30cc914010ba2ed"" + }, + ""mac"": 123, // Not a string, but a number + }, + ""id"": ""3b647634-1234-cd02-e93c-02e1c0f54faa"", + ""version"": 3, + }") + ); + + Assert.Throws(() => + load(@"{ + ""address"": ""d80d933db45cc0cf69e9632090f8aaff635dc8e5"", + ""crypto"": { + ""cipher"": ""aes-128-ctr"", + ""cipherparams"": { + ""iv"": ""bc7f2ca23bfee0dd9725228ab2b0d98a"", + }, + ""ciphertext"": + ""535bab75c112fe327dc2e293f80297ff339e1e3cb76749615313cdc2aaa3b37a"", + ""kdf"": ""pbkdf2"", + ""kdfparams"": { + ""c"": 10240, + ""dklen"": 32, + ""prf"": ""hmac-sha256"", + ""salt"": + ""3eeaaf35da70928387cae1ead31ed782b1135d7578a89d95e30cc914010ba2ed"" + }, + ""mac"": ""Not a hexadecimal string"", + }, + ""id"": ""3b647634-1234-cd02-e93c-02e1c0f54faa"", + ""version"": 3, + }") + ); + } + #pragma warning restore MEN003 + + [Fact] + public void WriteJson() + { + string json; + using (var stream = new MemoryStream()) + { + Fixture.WriteJson(stream); + json = Encoding.UTF8.GetString(stream.ToArray()); + } + + // TODO: More decent tests should be written. + ProtectedPrivateKey key = ProtectedPrivateKey.FromJson(json); + Assert.Equal(AddressFixture, key.Address); + Assert.Equal(AddressFixture, key.Unprotect(PassphraseFixture).PublicKey.ToAddress()); + } + } +} diff --git a/Libplanet.Tests/Libplanet.Tests.csproj b/Libplanet.Tests/Libplanet.Tests.csproj index 61123cfc583..375d95f3945 100644 --- a/Libplanet.Tests/Libplanet.Tests.csproj +++ b/Libplanet.Tests/Libplanet.Tests.csproj @@ -19,7 +19,7 @@ + '$(MSBuildVersion)'>='16.3.0'"> netcoreapp3.0 diff --git a/Libplanet.Tests/Store/StoreTest.cs b/Libplanet.Tests/Store/StoreTest.cs index 03e20acf98c..ca2048d22dd 100644 --- a/Libplanet.Tests/Store/StoreTest.cs +++ b/Libplanet.Tests/Store/StoreTest.cs @@ -765,8 +765,8 @@ int txNonce Assert.Single(tx.Actions); AtomicityTestAction action = tx.Actions[0]; Assert.Equal( - md5Hasher.ComputeHash(action.ArbitraryBytes.ToBuilder().ToArray()), - action.Md5Digest.ToBuilder().ToArray() + md5Hasher.ComputeHash(action.ArbitraryBytes.ToArray()), + action.Md5Digest.ToArray() ); } } @@ -780,8 +780,8 @@ private class AtomicityTestAction : IAction public IValue PlainValue => new Bencodex.Types.Dictionary(new Dictionary { - { (Text)"bytes", new Binary(ArbitraryBytes.ToBuilder().ToArray()) }, - { (Text)"md5", new Binary(Md5Digest.ToBuilder().ToArray()) }, + { (Text)"bytes", new Binary(ArbitraryBytes.ToArray()) }, + { (Text)"md5", new Binary(Md5Digest.ToArray()) }, }); public void LoadPlainValue(IValue plainValue) diff --git a/Libplanet.Tests/TestUtils.cs b/Libplanet.Tests/TestUtils.cs index 146d37244bf..bfd6708adde 100644 --- a/Libplanet.Tests/TestUtils.cs +++ b/Libplanet.Tests/TestUtils.cs @@ -1,6 +1,7 @@ using System; using System.Collections; using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; using System.Security.Cryptography; using System.Text; @@ -56,6 +57,14 @@ string Repr(byte[] bytes) } } + internal static void AssertBytesEqual( + ImmutableArray expected, + ImmutableArray actual + ) + { + AssertBytesEqual(expected.ToArray(), actual.ToArray()); + } + internal static void AssertBytesEqual(TxId expected, TxId actual) { AssertBytesEqual(expected.ToByteArray(), actual.ToByteArray()); diff --git a/Libplanet/ByteUtil.cs b/Libplanet/ByteUtil.cs index 676dc2c16ad..aa001ce42d7 100644 --- a/Libplanet/ByteUtil.cs +++ b/Libplanet/ByteUtil.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Immutable; using System.Diagnostics.Contracts; using System.Linq; @@ -76,6 +77,20 @@ public static string Hex(byte[] bytes) return s.Replace("-", string.Empty).ToLower(); } + /// + /// Renders a hexadecimal string from a array. + /// + /// A array to renders + /// the corresponding hexadecimal string. It must not be null. + /// + /// A hexadecimal string which encodes the given + /// . + /// Thrown when the given + /// is null. + [Pure] + public static string Hex(in ImmutableArray bytes) => + Hex(bytes.ToArray()); + /// /// Calculates a deterministic hash code from a given /// . It is mostly used to implement diff --git a/Libplanet/Crypto/PrivateKey.cs b/Libplanet/Crypto/PrivateKey.cs index 7f7644a3520..4f9efca8a2c 100644 --- a/Libplanet/Crypto/PrivateKey.cs +++ b/Libplanet/Crypto/PrivateKey.cs @@ -62,9 +62,8 @@ public PrivateKey() /// A valid array that /// encodes an ECDSA private key. /// - /// A valid array for - /// a can be encoded using - /// property. + /// A valid array for a . + /// Can be encoded using property. /// /// public PrivateKey(byte[] privateKey) diff --git a/Libplanet/KeyStore/Ciphers/Aes128Ctr.cs b/Libplanet/KeyStore/Ciphers/Aes128Ctr.cs new file mode 100644 index 00000000000..c5eabe4c658 --- /dev/null +++ b/Libplanet/KeyStore/Ciphers/Aes128Ctr.cs @@ -0,0 +1,150 @@ +using System; +using System.Collections.Immutable; +using System.Diagnostics.Contracts; +using System.Linq; +using System.Text.Json; +using Org.BouncyCastle.Crypto; +using Org.BouncyCastle.Crypto.Parameters; +using Org.BouncyCastle.Security; + +namespace Libplanet.KeyStore.Ciphers +{ + /// + /// AES-128-CTR (AES 128-bit in counter moder). + /// + [Pure] + public sealed class Aes128Ctr : ICipher + { + /// + /// Creates an instance with the given . + /// + /// Initialization vector. + /// Thrown when the length of is + /// invalid. + public Aes128Ctr(byte[] iv) + : this(ImmutableArray.Create(iv)) + { + } + + /// + /// Creates an instance with the given . + /// + /// Initialization vector. + /// Thrown when the length of is + /// invalid. + public Aes128Ctr(in ImmutableArray iv) + { + if (iv.Length > 16) + { + throw new ArgumentOutOfRangeException(nameof(iv), "IV cannot be greater than 16."); + } + else if (iv.IsEmpty) + { + throw new ArgumentException("IV should be provided.", nameof(iv)); + } + + Iv = iv; + } + + /// + /// Initialization vector. + /// + [Pure] + public ImmutableArray Iv { get; } + + /// + [Pure] + public ImmutableArray Encrypt( + in ImmutableArray key, + in ImmutableArray plaintext + ) => + Cipher(true, key, plaintext); + + /// + [Pure] + public ImmutableArray Decrypt( + in ImmutableArray key, + in ImmutableArray ciphertext + ) => + Cipher(false, key, ciphertext); + + /// + public string WriteJson(Utf8JsonWriter writer) + { + writer.WriteStartObject(); + writer.WriteString("iv", ByteUtil.Hex(Iv)); + writer.WriteEndObject(); + return "aes-128-ctr"; + } + + internal static ICipher FromJson(in JsonElement paramsElement) + { + if (!paramsElement.TryGetProperty("iv", out JsonElement ivElement)) + { + throw new InvalidKeyJsonException( + "The \"cipherparams\" field must have an \"iv\" field." + ); + } + + string ivString; + try + { + ivString = ivElement.GetString(); + } + catch (InvalidOperationException) + { + throw new InvalidKeyJsonException("The \"iv\" field must be a string."); + } + + if (ivString is null) + { + throw new InvalidKeyJsonException( + "The \"iv\" field must not be null, but a string." + ); + } + + byte[] iv; + try + { + iv = ByteUtil.ParseHex(ivString); + } + catch (Exception e) + { + throw new InvalidKeyJsonException( + "The \"iv\" field must be a hexadecimal string of bytes.\n" + e + ); + } + + return new Aes128Ctr(iv); + } + + private ImmutableArray Cipher( + in bool encrypt, + in ImmutableArray key, + in ImmutableArray input + ) + { + int keyLength = key.Length; + if (keyLength != 16 && keyLength != 24 && keyLength != 32) + { + throw new ArgumentException( + "Key length must be one of 16/24/32 bytes (128/192/256 bits, respectively).", + nameof(key) + ); + } + + // FIXME: Rather than depending on BouncyCastle, which is a pure C# implementation, + // it's better to use .NET Standard's System.Security.Cryptography API. + IBufferedCipher cipher = CipherUtilities.GetCipher("AES/CTR/NoPadding"); + + KeyParameter keyParam = ParameterUtilities.CreateKeyParameter( + "AES128", + key.ToArray() + ); + byte[] iv = Iv.ToArray(); + ICipherParameters cipherParams = new ParametersWithIV(keyParam, iv); + cipher.Init(encrypt, cipherParams); + return cipher.DoFinal(input.ToArray()).ToImmutableArray(); + } + } +} diff --git a/Libplanet/KeyStore/Ciphers/ICipher.cs b/Libplanet/KeyStore/Ciphers/ICipher.cs new file mode 100644 index 00000000000..cbbeaffc8b1 --- /dev/null +++ b/Libplanet/KeyStore/Ciphers/ICipher.cs @@ -0,0 +1,48 @@ +using System.Collections.Immutable; +using System.Diagnostics.Contracts; +using System.Text.Json; + +namespace Libplanet.KeyStore.Ciphers +{ + /// + /// An interface to define symmetric cipher algorithm. + /// + public interface ICipher + { + /// + /// Encrypts the given using the given . + /// + /// A symmetric key. + /// An immutable array to encrypt. + /// The ciphertext made from the + /// using the . + [Pure] + ImmutableArray Encrypt( + in ImmutableArray key, + in ImmutableArray plaintext + ); + + /// + /// Decrypts the given using the given . + /// + /// A symmetric key. + /// An immutable array to decrypt. + /// The plain text decrypted from the + /// using the . + [Pure] + ImmutableArray Decrypt( + in ImmutableArray key, + in ImmutableArray ciphertext + ); + + /// + /// Dumps the cipher parameters as a JSON representation according to Ethereum's + /// Web3 + /// Secret Storage Definition. + /// + /// A JSON writer which has not begun object nor array. + /// A unique identifier of the cipher algorithm. This is going to be the + /// crypto.cipher field in the key JSON file. + string WriteJson(Utf8JsonWriter writer); + } +} diff --git a/Libplanet/KeyStore/IncorrectPassphraseException.cs b/Libplanet/KeyStore/IncorrectPassphraseException.cs new file mode 100644 index 00000000000..307f53410f0 --- /dev/null +++ b/Libplanet/KeyStore/IncorrectPassphraseException.cs @@ -0,0 +1,48 @@ +using System; +using System.Collections.Immutable; + +namespace Libplanet.KeyStore +{ + /// + /// The exception that is thrown when a user input passphrase (i.e., password) is incorrect. + /// + public class IncorrectPassphraseException : ArgumentException + { + /// + /// Creates a new object. + /// + /// The error message that explains the reason for the exception. + /// + /// The name of the parameter that caused the current exception. + /// + /// The expected MAC of the correct passphrase. + /// It is automatically included to the string. + /// The actual MAC of the user input passphrase. + /// It is automatically included to the string. + public IncorrectPassphraseException( + string message, + string paramName, + in ImmutableArray expectedMac, + in ImmutableArray inputMac + ) + : base( + $"{message}\nExpected MAC: {ByteUtil.Hex(expectedMac)}\n" + + $"Input MAC: {ByteUtil.Hex(inputMac)}", + paramName + ) + { + ExpectedMac = expectedMac; + InputMac = inputMac; + } + + /// + /// The expected MAC of the correct passphrase. + /// + public ImmutableArray ExpectedMac { get; } + + /// + /// The actual MAC of the user input passphrase. + /// + public ImmutableArray InputMac { get; } + } +} diff --git a/Libplanet/KeyStore/InvalidKeyJsonException.cs b/Libplanet/KeyStore/InvalidKeyJsonException.cs new file mode 100644 index 00000000000..e88d55f7a3a --- /dev/null +++ b/Libplanet/KeyStore/InvalidKeyJsonException.cs @@ -0,0 +1,19 @@ +using System; + +namespace Libplanet.KeyStore +{ + /// + /// The exception that is thrown when a key JSON is invalid, e.g., missing field. + /// + public class InvalidKeyJsonException : KeyJsonException + { + /// + /// Creates a new instance. + /// + /// A detailed exception message. + public InvalidKeyJsonException(string message) + : base(message) + { + } + } +} diff --git a/Libplanet/KeyStore/Kdfs/IKdf.cs b/Libplanet/KeyStore/Kdfs/IKdf.cs new file mode 100644 index 00000000000..672f36cd38f --- /dev/null +++ b/Libplanet/KeyStore/Kdfs/IKdf.cs @@ -0,0 +1,30 @@ +using System.Collections.Immutable; +using System.Diagnostics.Contracts; +using System.Text.Json; + +namespace Libplanet.KeyStore.Kdfs +{ + /// + /// An interface to form key derivation functions (KDF) that are used to derive a valid + /// cryptographic key from a user input passphrase (i.e., password). + /// + public interface IKdf + { + /// + /// Derives a cryptographic key in s from a user input + /// . + /// + /// A user input passphrase. + /// A derived cryptographic key. + [Pure] + ImmutableArray Derive(string passphrase); + + /// + /// Dumps the KDF parameters as a JSON representation. + /// + /// A JSON writer which has not begun object nor array. + /// A unique identifier of the KDF. This is going to be the + /// crypto.kdf field in the key JSON file. + string WriteJson(Utf8JsonWriter writer); + } +} diff --git a/Libplanet/KeyStore/Kdfs/Pbkdf2.cs b/Libplanet/KeyStore/Kdfs/Pbkdf2.cs new file mode 100644 index 00000000000..2f9b02770b9 --- /dev/null +++ b/Libplanet/KeyStore/Kdfs/Pbkdf2.cs @@ -0,0 +1,206 @@ +using System; +using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; +using System.Diagnostics.Contracts; +using System.Linq; +using System.Text.Json; +using Org.BouncyCastle.Crypto; +using Org.BouncyCastle.Crypto.Digests; +using Org.BouncyCastle.Crypto.Generators; +using Org.BouncyCastle.Crypto.Parameters; + +namespace Libplanet.KeyStore.Kdfs +{ + /// + /// PBKDF2. + /// + /// PRF (pseudorandom function) to use, e.g., + /// . + [SuppressMessage( + "Microsoft.StyleCop.CSharp.ReadabilityRules", + "SA1402", + Justification = "There are just generic & non-generic versions of the same name classes.")] + [Pure] + public sealed class Pbkdf2 : IKdf + where T : GeneralDigest, new() + { + /// + /// Configures parameters of PBKDF2. + /// + /// The number of iterations desired. + /// Corresponds to c. + /// A cryptographic salt. + /// The desired byte-length of the derived key. + /// Corresponds to dkLen except that it's not bit-wise but byte-wise. + public Pbkdf2(int iterations, byte[] salt, int keyLength) + : this(iterations, ImmutableArray.Create(salt, 0, salt.Length), keyLength) + { + } + + /// + /// Configures parameters of PBKDF2. + /// + /// The number of iterations desired. + /// Corresponds to c. + /// A cryptographic salt. + /// The desired byte-length of the derived key. + /// Corresponds to dkLen except that it's not bit-wise but byte-wise. + public Pbkdf2(int iterations, in ImmutableArray salt, int keyLength) + { + Iterations = iterations; + Salt = salt; + KeyLength = keyLength; + } + + /// + /// The number of iterations desired. Corresponds to c. + /// + public int Iterations { get; } + + /// + /// The desired byte-length of the derived key. + /// Corresponds to dkLen except that it's not bit-wise but byte-wise. + /// + public int KeyLength { get; } + + /// + /// A cryptographic salt. + /// + public ImmutableArray Salt { get; } + + /// + [Pure] + public ImmutableArray Derive(string passphrase) + { + var pdb = new Pkcs5S2ParametersGenerator(new T()); + pdb.Init( + PbeParametersGenerator.Pkcs5PasswordToBytes(passphrase.ToCharArray()), + Salt.ToArray(), + Iterations + ); + var key = (KeyParameter)pdb.GenerateDerivedMacParameters(KeyLength * 8); + return ImmutableArray.Create(key.GetKey(), 0, KeyLength); + } + + /// + public string WriteJson(Utf8JsonWriter writer) + { + writer.WriteStartObject(); + writer.WriteNumber("c", Iterations); + writer.WriteNumber("dklen", KeyLength); + writer.WriteString( + "prf", + "hmac-" + new T().AlgorithmName.ToLower().Replace("-", string.Empty) + ); + writer.WriteString("salt", ByteUtil.Hex(Salt)); + writer.WriteEndObject(); + return "pbkdf2"; + } + } + + internal static class Pbkdf2 + { + internal static IKdf FromJson(in JsonElement element) + { + if (!element.TryGetProperty("c", out JsonElement c)) + { + throw new InvalidKeyJsonException( + "The \"kdfparams\" field must have a \"c\" field, the number of iterations." + ); + } + + if (c.ValueKind != JsonValueKind.Number || !c.TryGetInt32(out int iterations)) + { + throw new InvalidKeyJsonException( + "The \"c\" field, the number of iterations, must be a number." + ); + } + + if (!element.TryGetProperty("dklen", out JsonElement dklen)) + { + throw new InvalidKeyJsonException( + "The \"kdfparams\" field must have a \"dklen\" field, " + + "the length of key in bytes." + ); + } + + if (dklen.ValueKind != JsonValueKind.Number || + !dklen.TryGetInt32(out int keyLength)) + { + throw new InvalidKeyJsonException( + "The \"dklen\" field, the length of key in bytes, must be a number." + ); + } + + if (!element.TryGetProperty("salt", out JsonElement saltElement)) + { + throw new InvalidKeyJsonException( + "The \"kdfparams\" field must have a \"salt\" field." + ); + } + + string saltString; + try + { + saltString = saltElement.GetString(); + } + catch (InvalidOperationException) + { + throw new InvalidKeyJsonException("The \"salt\" field must be a string."); + } + + byte[] salt; + try + { + salt = ByteUtil.ParseHex(saltString); + } + catch (ArgumentNullException) + { + throw new InvalidKeyJsonException( + "The \"salt\" field must not be null, but a string." + ); + } + catch (Exception e) + { + throw new InvalidKeyJsonException( + "The \"salt\" field must be a hexadecimal string of bytes.\n" + e + ); + } + + if (!element.TryGetProperty("prf", out JsonElement prfElement)) + { + throw new InvalidKeyJsonException( + "The \"kdfparams\" field must have a \"prf\" field." + ); + } + + string prf; + try + { + prf = prfElement.GetString(); + } + catch (InvalidOperationException) + { + throw new InvalidKeyJsonException( + "The \"prf\" field must be a string." + ); + } + + switch (prf) + { + case "hmac-sha256": + return new Pbkdf2(iterations, salt, keyLength); + + case null: + throw new InvalidKeyJsonException( + "The \"prf\" field must not be null, but a string." + ); + + default: + throw new UnsupportedKeyJsonException( + $"Unsupported \"prf\" type: \"{prf}\"." + ); + } + } + } +} diff --git a/Libplanet/KeyStore/KeyJsonException.cs b/Libplanet/KeyStore/KeyJsonException.cs new file mode 100644 index 00000000000..90b6d67df90 --- /dev/null +++ b/Libplanet/KeyStore/KeyJsonException.cs @@ -0,0 +1,20 @@ +using System; + +namespace Libplanet.KeyStore +{ + /// + /// Serves as the base class for exceptions thrown by + /// method. + /// + public abstract class KeyJsonException : Exception + { + /// + /// Creates a new instance with a message. + /// + /// A detailed exception message. + protected KeyJsonException(string message) + : base(message) + { + } + } +} diff --git a/Libplanet/KeyStore/MismatchedAddressException.cs b/Libplanet/KeyStore/MismatchedAddressException.cs new file mode 100644 index 00000000000..cf184f9e466 --- /dev/null +++ b/Libplanet/KeyStore/MismatchedAddressException.cs @@ -0,0 +1,44 @@ +using System; + +namespace Libplanet.KeyStore +{ + /// + /// The exception that is thrown when an unprotected private key's actual address does + /// not match to the expected address. + /// + public class MismatchedAddressException : InvalidOperationException + { + /// + /// Creates a new object. + /// + /// The error message that explains the reason for the exception. + /// + /// The expected address of a protected private key. + /// It is automatically included to the string. + /// The actual address of an unprotected private key. + /// It is automatically included to the string. + public MismatchedAddressException( + string message, + in Address expectedAddress, + in Address actualAddress + ) + : base( + $"{message}\nExpected address: {expectedAddress}\n" + + $"Actual address: {actualAddress}" + ) + { + ExpectedAddress = expectedAddress; + ActualAddress = actualAddress; + } + + /// + /// The expected address of the protected private key. + /// + public Address ExpectedAddress { get; } + + /// + /// The actual address of the unprotected private key. + /// + public Address ActualAddress { get; } + } +} diff --git a/Libplanet/KeyStore/ProtectedPrivateKey.cs b/Libplanet/KeyStore/ProtectedPrivateKey.cs new file mode 100644 index 00000000000..8c01a108da1 --- /dev/null +++ b/Libplanet/KeyStore/ProtectedPrivateKey.cs @@ -0,0 +1,404 @@ +using System; +using System.Collections.Immutable; +using System.Diagnostics.Contracts; +using System.IO; +using System.Linq; +using System.Security.Cryptography; +using System.Text.Json; +using Libplanet.Crypto; +using Libplanet.KeyStore.Ciphers; +using Libplanet.KeyStore.Kdfs; +using Org.BouncyCastle.Crypto.Digests; + +namespace Libplanet.KeyStore +{ + /// + /// Protects a with a passphrase (i.e., password). + /// + public sealed class ProtectedPrivateKey + { + /// + /// Loads a protected private key. + /// + /// The address of the protected private key. + /// A key derivation function to derive a symmetric key to decrypt + /// a . + /// MAC digest to check if a derived key is correct or not. + /// A symmetric cipher to decrypt a . + /// An encrypted . + public ProtectedPrivateKey( + Address address, + IKdf kdf, + byte[] mac, + ICipher cipher, + byte[] ciphertext + ) + : this( + address, + kdf, + ImmutableArray.Create(mac), + cipher, + ImmutableArray.Create(ciphertext) + ) + { + } + + /// + /// Loads a protected private key. + /// + /// The address of the protected private key. + /// A key derivation function to derive a symmetric key to decrypt + /// a . + /// MAC digest to check if a derived key is correct or not. + /// A symmetric cipher to decrypt a . + /// An encrypted . + public ProtectedPrivateKey( + Address address, + IKdf kdf, + ImmutableArray mac, + ICipher cipher, + ImmutableArray ciphertext + ) + { + Address = address; + Kdf = kdf; + Mac = mac; + Cipher = cipher; + Ciphertext = ciphertext; + } + + /// + /// The address of the protected private key. + /// + public Address Address { get; } + + /// + /// A key derivation function to derive a symmetric key to decrypt + /// a . + /// + [Pure] + public IKdf Kdf { get; } + + [Pure] + public ImmutableArray Mac { get; } + + /// + /// A symmetric cipher to decrypt a . + /// + [Pure] + public ICipher Cipher { get; } + + /// + /// An encrypted . + /// + [Pure] + public ImmutableArray Ciphertext { get; } + + /// + /// Protects a bare using a user input + /// . + /// + /// A bare private key to protect. + /// A user input passphrase (i.e., password). + /// A passphrase-protected private key. + [Pure] + public static ProtectedPrivateKey Protect(PrivateKey privateKey, string passphrase) + { + var salt = new byte[32]; + using (RandomNumberGenerator rng = RandomNumberGenerator.Create()) + { + rng.GetBytes(salt); + var kdf = new Pbkdf2(10240, salt, 32); + ImmutableArray derivedKey = kdf.Derive(passphrase); + ImmutableArray encKey = MakeEncryptionKey(derivedKey); + var iv = new byte[16]; + rng.GetBytes(iv); + var cipher = new Aes128Ctr(iv); + ImmutableArray ciphertext = cipher.Encrypt( + encKey, + ImmutableArray.Create(privateKey.ByteArray) + ); + ImmutableArray mac = CalculateMac(derivedKey, ciphertext); + Address address = privateKey.PublicKey.ToAddress(); + return new ProtectedPrivateKey(address, kdf, mac, cipher, ciphertext); + } + } + + /// + /// Loads a from a JSON, according to Ethereum's + /// Web3 + /// Secret Storage Definition. + /// + /// A JSON string that encodes a . + /// + /// A protected private key loaded from the given . + /// + /// Thrown when the given is not + /// a valid JSON. + /// Thrown when the given key data lacks some + /// required fields or consists of wrong types. + /// Thrown when the given key data depends on + /// an unsupported features (e.g., KDF). + public static ProtectedPrivateKey FromJson(string json) + { + var options = new JsonDocumentOptions + { + AllowTrailingCommas = true, + CommentHandling = JsonCommentHandling.Skip, + }; + + using (JsonDocument doc = JsonDocument.Parse(json, options)) + { + JsonElement rootElement = doc.RootElement; + if (rootElement.ValueKind != JsonValueKind.Object) + { + throw new InvalidKeyJsonException( + "The root of the key JSON must be an object, but it is a/an " + + $"{rootElement.ValueKind}." + ); + } + + if (!rootElement.TryGetProperty("version", out JsonElement versionElement)) + { + throw new InvalidKeyJsonException( + "The key JSON must contain \"version\" field, but it lacks." + ); + } + + if (versionElement.ValueKind != JsonValueKind.Number || + !versionElement.TryGetDecimal(out decimal versionNum)) + { + throw new InvalidKeyJsonException("The \"version\" field must be a number."); + } + else if (versionNum != 3) + { + throw new UnsupportedKeyJsonException( + $"The key JSON format version {versionNum} is unsupported; " + + "Only version 3 is supported." + ); + } + + string GetStringProperty(JsonElement element, string fieldName) + { + if (!element.TryGetProperty(fieldName, out JsonElement fieldElement)) + { + throw new InvalidKeyJsonException( + $"The key JSON must contain \"{fieldName}\" field, but it lacks." + ); + } + + string str; + try + { + str = fieldElement.GetString(); + } + catch (InvalidOperationException) + { + throw new InvalidKeyJsonException( + $"The \"{fieldName}\" field must be a string." + ); + } + + if (str is null) + { + throw new InvalidKeyJsonException( + $"The \"{fieldName}\" field must not be null, but a string." + ); + } + + return str; + } + + JsonElement GetObjectProperty(JsonElement element, string fieldName) + { + if (!element.TryGetProperty(fieldName, out var fieldElement)) + { + throw new InvalidKeyJsonException( + $"The key JSON must contain \"{fieldName}\" field, but it lacks." + ); + } + else if (fieldElement.ValueKind != JsonValueKind.Object) + { + throw new InvalidKeyJsonException( + $"The \"{fieldName}\" field must be an object, but it is a/an " + + $"{fieldElement.ValueKind}." + ); + } + + return fieldElement; + } + + byte[] GetHexProperty(JsonElement element, string fieldName) + { + string str = GetStringProperty(element, fieldName); + byte[] bytes; + try + { + bytes = ByteUtil.ParseHex(str); + } + catch (Exception e) + { + throw new InvalidKeyJsonException( + $"The \"{fieldName}\" field must be a hexadecimal string.\n{e}" + ); + } + + return bytes; + } + + JsonElement crypto = GetObjectProperty(rootElement, "crypto"); + string cipherType = GetStringProperty(crypto, "cipher"); + JsonElement cipherParamsElement = GetObjectProperty(crypto, "cipherparams"); + byte[] ciphertext = GetHexProperty(crypto, "ciphertext"); + byte[] mac = GetHexProperty(crypto, "mac"); + string kdfType = GetStringProperty(crypto, "kdf"); + JsonElement kdfParamsElement = GetObjectProperty(crypto, "kdfparams"); + byte[] addressBytes = GetHexProperty(rootElement, "address"); + Address address; + try + { + address = new Address(addressBytes); + } + catch (ArgumentException e) + { + throw new InvalidKeyJsonException( + "The \"address\" field must contain an Ethereum-style address which " + + "consists of 40 hexadecimal letters: " + e + ); + } + + ICipher cipher; + switch (cipherType) + { + case "aes-128-ctr": + cipher = Aes128Ctr.FromJson(cipherParamsElement); + break; + + default: + throw new UnsupportedKeyJsonException( + $"Unsupported cipher type: \"{cipherType}\"." + ); + } + + IKdf kdf; + switch (kdfType) + { + case "pbkdf2": + kdf = Pbkdf2.FromJson(kdfParamsElement); + break; + + default: + throw new UnsupportedKeyJsonException( + $"Unsupported cipher type: \"{kdfType}\"." + ); + } + + return new ProtectedPrivateKey(address, kdf, mac, cipher, ciphertext); + } + } + + /// + /// Gets the protected using a user input + /// . + /// + /// A user input passphrase (i.e., password). + /// A bare . + [Pure] + public PrivateKey Unprotect(string passphrase) + { + ImmutableArray derivedKey = Kdf.Derive(passphrase); + var mac = CalculateMac(derivedKey, Ciphertext); + if (!Mac.SequenceEqual(mac)) + { + throw new IncorrectPassphraseException( + "The input passphrase is incorrect.", + nameof(passphrase), + Mac, + mac + ); + } + + ImmutableArray encKey = MakeEncryptionKey(derivedKey); + ImmutableArray plaintext = Cipher.Decrypt(encKey, Ciphertext); + + var key = new PrivateKey(plaintext.ToArray()); + Address actualAddress = key.PublicKey.ToAddress(); + if (!Address.Equals(actualAddress)) + { + throw new MismatchedAddressException( + "The actual address of the unprotected private key does not match to " + + "the expected address of the protected private key.", + Address, + actualAddress + ); + } + + return key; + } + + /// + /// Dumps the cipher parameters as a JSON representation according to Ethereum's + /// Web3 + /// Secret Storage Definition. + /// + /// A JSON writer which has not begun object nor array. + /// A unique identifier, which goes to the id field in the key JSON + /// file. If null (which is default) it is random-generated. + public void WriteJson(Utf8JsonWriter writer, [Pure] in Guid? id = null) + { + writer.WriteStartObject(); + writer.WriteNumber("version", 3); + writer.WriteString("id", (id ?? Guid.NewGuid()).ToString().ToLower()); + writer.WriteString("address", Address.ToHex().ToLower()); + writer.WriteStartObject("crypto"); + writer.WriteString("ciphertext", ByteUtil.Hex(Ciphertext)); + writer.WritePropertyName("cipherparams"); + string cipherName = Cipher.WriteJson(writer); + writer.WriteString("cipher", cipherName); + writer.WritePropertyName("kdfparams"); + string kdfName = Kdf.WriteJson(writer); + writer.WriteString("kdf", kdfName); + writer.WriteString("mac", ByteUtil.Hex(Mac)); + writer.WriteEndObject(); + writer.WriteEndObject(); + } + + /// + /// Dumps the cipher parameters as a JSON representation according to Ethereum's + /// Web3 + /// Secret Storage Definition. + /// + /// The destination for writing JSON text. + /// A unique identifier, which goes to the id field in the key JSON + /// file. If null (which is default) it is random-generated. + public void WriteJson(Stream stream, [Pure] in Guid? id = null) + { + using (var writer = new Utf8JsonWriter(stream)) + { + WriteJson(writer, id); + } + } + + private static ImmutableArray MakeEncryptionKey(ImmutableArray derivedKey) + { + const int keySubBytes = 16; + return ImmutableArray.Create(derivedKey, 0, derivedKey.Length - keySubBytes); + } + + private static ImmutableArray CalculateMac( + ImmutableArray derivedKey, + ImmutableArray ciphertext + ) + { + const int keySubBytes = 16; + var seal = new byte[keySubBytes + ciphertext.Length]; + derivedKey.CopyTo(derivedKey.Length - keySubBytes, seal, 0, keySubBytes); + ciphertext.CopyTo(seal, keySubBytes); + var digest = new KeccakDigest(256); + var mac = new byte[digest.GetDigestSize()]; + digest.BlockUpdate(seal, 0, seal.Length); + digest.DoFinal(mac, 0); + return ImmutableArray.Create(mac); + } + } +} diff --git a/Libplanet/KeyStore/UnsupportedKeyJsonException.cs b/Libplanet/KeyStore/UnsupportedKeyJsonException.cs new file mode 100644 index 00000000000..6deea3a622e --- /dev/null +++ b/Libplanet/KeyStore/UnsupportedKeyJsonException.cs @@ -0,0 +1,18 @@ +namespace Libplanet.KeyStore +{ + /// + /// The exception that is thrown when a key JSON is valid but uses an unsupported feature, + /// e.g., unsupported cipher algorithm. + /// + public class UnsupportedKeyJsonException : KeyJsonException + { + /// + /// Creates a new instance with a message. + /// + /// A detailed exception message. + public UnsupportedKeyJsonException(string message) + : base(message) + { + } + } +} diff --git a/Libplanet/Libplanet.csproj b/Libplanet/Libplanet.csproj index 8f680b58eaf..011dd81554e 100644 --- a/Libplanet/Libplanet.csproj +++ b/Libplanet/Libplanet.csproj @@ -48,7 +48,7 @@ https://docs.libplanet.io/ + '$(MSBuildVersion)'>='16.3.0'"> netstandard2.0;netcoreapp3.0 @@ -93,6 +93,7 @@ https://docs.libplanet.io/ +