/// <summary> /// Loads a <see cref="ProtectedPrivateKey"/> from a JSON, according to Ethereum's /// <a href="https://github.com/ethereum/wiki/wiki/Web3-Secret-Storage-Definition">Web3 /// Secret Storage Definition</a>. /// </summary> /// <param name="json">A JSON string that encodes a <see cref="ProtectedPrivateKey"/>. /// </param> /// <returns>A protected private key loaded from the given <paramref name="json"/>. /// </returns> /// <exception cref="JsonException">Thrown when the given <paramref name="json"/> is not /// a valid JSON.</exception> /// <exception cref="InvalidKeyJsonException">Thrown when the given key data lacks some /// required fields or consists of wrong types.</exception> /// <exception cref="UnsupportedKeyJsonException">Thrown when the given key data depends on /// an unsupported features (e.g., KDF).</exception> 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 ); } var cipher = cipherType switch { "aes-128-ctr" => Aes128Ctr.FromJson(cipherParamsElement), _ => throw new UnsupportedKeyJsonException( $"Unsupported cipher type: \"{cipherType}\".") }; IKdf kdf; try { kdf = kdfType switch { "pbkdf2" => Pbkdf2.FromJson(kdfParamsElement), "scrypt" => Scrypt.FromJson(kdfParamsElement), _ => throw new UnsupportedKeyJsonException( $"Unsupported cipher type: \"{kdfType}\".") }; } catch (ArgumentException e) { throw new InvalidKeyJsonException(e.Message); } return(new ProtectedPrivateKey(address, kdf, mac, cipher, ciphertext)); }
public void FromJson() { var options = new JsonDocumentOptions { AllowTrailingCommas = true, CommentHandling = JsonCommentHandling.Skip, }; Pbkdf2 <Sha256Digest> Load(string json) { using (JsonDocument doc = JsonDocument.Parse(json, options)) { return((Pbkdf2 <Sha256Digest>)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 <InvalidKeyJsonException>(() => Load(@" { // ""c"": 10240, // lacks ""dklen"": 32, ""prf"": ""hmac-sha256"", ""salt"": ""3eeaaf35da70928387cae1ead31ed782b1135d7578a89d95e30cc914010ba2ed"", } ") ); Assert.Throws <InvalidKeyJsonException>(() => Load(@" { ""c"": true, // not a number ""dklen"": 32, ""prf"": ""hmac-sha256"", ""salt"": ""3eeaaf35da70928387cae1ead31ed782b1135d7578a89d95e30cc914010ba2ed"", } ") ); Assert.Throws <InvalidKeyJsonException>(() => Load(@" { ""c"": null, // not a number, but null ""dklen"": 32, ""prf"": ""hmac-sha256"", ""salt"": ""3eeaaf35da70928387cae1ead31ed782b1135d7578a89d95e30cc914010ba2ed"", } ") ); Assert.Throws <InvalidKeyJsonException>(() => Load(@" { ""c"": 10240, // ""dklen"": 32, // lacks ""prf"": ""hmac-sha256"", ""salt"": ""3eeaaf35da70928387cae1ead31ed782b1135d7578a89d95e30cc914010ba2ed"", } ") ); Assert.Throws <InvalidKeyJsonException>(() => Load(@" { ""c"": 10240, ""dklen"": false, // not a number ""prf"": ""hmac-sha256"", ""salt"": ""3eeaaf35da70928387cae1ead31ed782b1135d7578a89d95e30cc914010ba2ed"", } ") ); Assert.Throws <InvalidKeyJsonException>(() => Load(@" { ""c"": 10240, ""dklen"": null, // not a number, but null ""prf"": ""hmac-sha256"", ""salt"": ""3eeaaf35da70928387cae1ead31ed782b1135d7578a89d95e30cc914010ba2ed"", } ") ); Assert.Throws <InvalidKeyJsonException>(() => Load(@" { ""c"": 10240, ""dklen"": 32, // ""prf"": ""hmac-sha256"", // lacks ""salt"": ""3eeaaf35da70928387cae1ead31ed782b1135d7578a89d95e30cc914010ba2ed"", } ") ); Assert.Throws <InvalidKeyJsonException>(() => Load(@" { ""c"": 10240, ""dklen"": 32, ""prf"": 123, // not a string, but a number ""salt"": ""3eeaaf35da70928387cae1ead31ed782b1135d7578a89d95e30cc914010ba2ed"", } ") ); Assert.Throws <UnsupportedKeyJsonException>(() => Load(@" { ""c"": 10240, ""dklen"": 32, ""prf"": ""hmac-sha512"", // unsupported prf ""salt"": ""3eeaaf35da70928387cae1ead31ed782b1135d7578a89d95e30cc914010ba2ed"", } ") ); Assert.Throws <InvalidKeyJsonException>(() => Load(@" { ""c"": 10240, ""dklen"": 32, ""prf"": ""hmac-sha256"", // ""salt"": ""..."", // lacks } ") ); Assert.Throws <InvalidKeyJsonException>(() => Load(@" { ""c"": 10240, ""dklen"": 32, ""prf"": ""hmac-sha256"", ""salt"": 1234, // not a string, but a number } ") ); Assert.Throws <InvalidKeyJsonException>(() => Load(@" { ""c"": 10240, ""dklen"": 32, ""prf"": ""hmac-sha256"", ""salt"": ""not a hexadecimal string"", } ") ); Assert.Throws <InvalidKeyJsonException>(() => Load(@" { ""c"": 10240, ""dklen"": 32, ""prf"": ""hmac-sha256"", ""salt"": ""3eeaaf35da70928387cae1ead31ed782b1135d7578a89d95e30cc914010ba2e"", // salt: invalid length } ") ); }