From 6d8a13dda51469ccc69cfddc37edcd32067dca28 Mon Sep 17 00:00:00 2001 From: Hong Minhee Date: Wed, 30 Oct 2019 20:09:09 +0900 Subject: [PATCH] ProtectedPrivateKey.FromJson() method --- .../KeyStore/Ciphers/Aes128CtrTest.cs | 101 +++ .../KeyStore/Ciphers/AesCtrTest.cs | 36 -- Libplanet.Tests/KeyStore/Kdfs/Pbkdf2Test.cs | 185 ++++++ .../KeyStore/ProtectedPrivateKeyTest.cs | 581 ++++++++++++++++++ Libplanet/KeyStore/Ciphers/Aes128Ctr.cs | 42 ++ Libplanet/KeyStore/InvalidKeyJsonException.cs | 19 + Libplanet/KeyStore/Kdfs/Pbkdf2.cs | 113 ++++ Libplanet/KeyStore/KeyJsonException.cs | 20 + Libplanet/KeyStore/ProtectedPrivateKey.cs | 172 ++++++ .../KeyStore/UnsupportedKeyJsonException.cs | 18 + Libplanet/Libplanet.csproj | 1 + 11 files changed, 1252 insertions(+), 36 deletions(-) create mode 100644 Libplanet.Tests/KeyStore/Ciphers/Aes128CtrTest.cs delete mode 100644 Libplanet.Tests/KeyStore/Ciphers/AesCtrTest.cs create mode 100644 Libplanet/KeyStore/InvalidKeyJsonException.cs create mode 100644 Libplanet/KeyStore/KeyJsonException.cs create mode 100644 Libplanet/KeyStore/UnsupportedKeyJsonException.cs diff --git a/Libplanet.Tests/KeyStore/Ciphers/Aes128CtrTest.cs b/Libplanet.Tests/KeyStore/Ciphers/Aes128CtrTest.cs new file mode 100644 index 00000000000..bca866b6cd0 --- /dev/null +++ b/Libplanet.Tests/KeyStore/Ciphers/Aes128CtrTest.cs @@ -0,0 +1,101 @@ +using System; +using System.Collections.Immutable; +using System.Text.Json; +using Libplanet.KeyStore; +using Libplanet.KeyStore.Ciphers; +using Libplanet.KeyStore.Kdfs; +using Org.BouncyCastle.Crypto.Digests; +using Xunit; + +namespace Libplanet.Tests.KeyStore.Ciphers +{ + public class Aes128CtrTest : CipherTest + { + private Aes128Ctr _cipher; + + public Aes128CtrTest() + { + var random = new Random(); + var buffer = new byte[16]; + random.NextBytes(buffer); + _cipher = new Aes128Ctr(buffer.ToImmutableArray()); + } + + public override Aes128Ctr Cipher => _cipher; + + [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/AesCtrTest.cs b/Libplanet.Tests/KeyStore/Ciphers/AesCtrTest.cs deleted file mode 100644 index b44f3a5eea2..00000000000 --- a/Libplanet.Tests/KeyStore/Ciphers/AesCtrTest.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System; -using System.Collections.Immutable; -using Libplanet.KeyStore.Ciphers; -using Xunit; - -namespace Libplanet.Tests.KeyStore.Ciphers -{ - public class AesCtrTest : CipherTest - { - private Aes128Ctr _cipher; - - public AesCtrTest() - { - var random = new Random(); - var buffer = new byte[16]; - random.NextBytes(buffer); - _cipher = new Aes128Ctr(buffer.ToImmutableArray()); - } - - public override Aes128Ctr Cipher => _cipher; - - [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()) - ); - } - } -} diff --git a/Libplanet.Tests/KeyStore/Kdfs/Pbkdf2Test.cs b/Libplanet.Tests/KeyStore/Kdfs/Pbkdf2Test.cs index fbbf3bd4036..9ec786675d8 100644 --- a/Libplanet.Tests/KeyStore/Kdfs/Pbkdf2Test.cs +++ b/Libplanet.Tests/KeyStore/Kdfs/Pbkdf2Test.cs @@ -1,6 +1,9 @@ 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 { @@ -10,5 +13,187 @@ 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/ProtectedPrivateKeyTest.cs b/Libplanet.Tests/KeyStore/ProtectedPrivateKeyTest.cs index 6a1288129d5..8573e6de36f 100644 --- a/Libplanet.Tests/KeyStore/ProtectedPrivateKeyTest.cs +++ b/Libplanet.Tests/KeyStore/ProtectedPrivateKeyTest.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Immutable; using Libplanet.Crypto; using Libplanet.KeyStore; @@ -75,5 +76,585 @@ public void Protect() 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 } } diff --git a/Libplanet/KeyStore/Ciphers/Aes128Ctr.cs b/Libplanet/KeyStore/Ciphers/Aes128Ctr.cs index e5e27d8b2f4..b663467cdb8 100644 --- a/Libplanet/KeyStore/Ciphers/Aes128Ctr.cs +++ b/Libplanet/KeyStore/Ciphers/Aes128Ctr.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Immutable; using System.Diagnostics.Contracts; +using System.Text.Json; using Org.BouncyCastle.Crypto; using Org.BouncyCastle.Crypto.Parameters; using Org.BouncyCastle.Security; @@ -65,6 +66,47 @@ in ImmutableArray ciphertext ) => Cipher(false, key, ciphertext); + 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, 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/Pbkdf2.cs b/Libplanet/KeyStore/Kdfs/Pbkdf2.cs index 30ea906bfbb..3ea2498cb0f 100644 --- a/Libplanet/KeyStore/Kdfs/Pbkdf2.cs +++ b/Libplanet/KeyStore/Kdfs/Pbkdf2.cs @@ -1,5 +1,8 @@ +using System; using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; using System.Diagnostics.Contracts; +using System.Text.Json; using Org.BouncyCastle.Crypto; using Org.BouncyCastle.Crypto.Digests; using Org.BouncyCastle.Crypto.Generators; @@ -12,6 +15,10 @@ namespace Libplanet.KeyStore.Kdfs /// /// PRF (pseudorandom function) to use, e.g., /// . + [SuppressMessage( + "Microsoft.StyleCop.CSharp.ReadabilityRules", + "SA1402", + Justification = "There are just generic and non-generic verions of the same name classes.")] public sealed class Pbkdf2 : IKdf where T : GeneralDigest, new() { @@ -73,4 +80,110 @@ public ImmutableArray Derive(string passphrase) return ImmutableArray.Create(key.GetKey(), 0, KeyLength); } } + + 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/ProtectedPrivateKey.cs b/Libplanet/KeyStore/ProtectedPrivateKey.cs index 36cd076ab69..8b082e9f299 100644 --- a/Libplanet/KeyStore/ProtectedPrivateKey.cs +++ b/Libplanet/KeyStore/ProtectedPrivateKey.cs @@ -3,6 +3,7 @@ using System.Diagnostics.Contracts; using System.Linq; using System.Security.Cryptography; +using System.Text.Json; using Libplanet.Crypto; using Libplanet.KeyStore.Ciphers; using Libplanet.KeyStore.Kdfs; @@ -120,6 +121,177 @@ public static ProtectedPrivateKey Protect(PrivateKey privateKey, string passphra return new ProtectedPrivateKey(address, kdf, mac, cipher, ciphertext); } + /// + /// Loads a from a JSON. + /// + /// 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 /// . 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 0f8087ed012..011dd81554e 100644 --- a/Libplanet/Libplanet.csproj +++ b/Libplanet/Libplanet.csproj @@ -93,6 +93,7 @@ https://docs.libplanet.io/ +