[#60] Wallet tools in SDK #62
14 changed files with 296 additions and 1 deletions
|
@ -45,6 +45,7 @@
|
||||||
<PackageReference Include="Microsoft.Extensions.Options" Version="7.0.1" />
|
<PackageReference Include="Microsoft.Extensions.Options" Version="7.0.1" />
|
||||||
<PackageReference Include="System.Diagnostics.DiagnosticSource" Version="7.0.0" />
|
<PackageReference Include="System.Diagnostics.DiagnosticSource" Version="7.0.0" />
|
||||||
<PackageReference Include="System.Runtime.Caching" Version="7.0.0" />
|
<PackageReference Include="System.Runtime.Caching" Version="7.0.0" />
|
||||||
|
<PackageReference Include="System.Text.Json" Version="7.0.4" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|
31
src/FrostFS.SDK.Client/Tools/WalletTools.cs
Normal file
31
src/FrostFS.SDK.Client/Tools/WalletTools.cs
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
using System.Text.Json;
|
||||||
|
using System;
|
||||||
|
using FrostFS.SDK.Client.Wallets;
|
||||||
|
using FrostFS.SDK.Cryptography;
|
||||||
|
|
||||||
|
namespace FrostFS.SDK.Client;
|
||||||
|
|
||||||
|
public static class WalletTools
|
||||||
|
{
|
||||||
|
public static string GetWifFromWallet(string walletJsonText, byte[] password, int accountIndex = 0)
|
||||||
|
{
|
||||||
|
var wallet = JsonSerializer.Deserialize<Wallet>(walletJsonText) ?? throw new ArgumentException("Wrong wallet format");
|
||||||
|
|
||||||
|
if (wallet.Accounts == null || wallet.Accounts.Length < accountIndex + 1)
|
||||||
|
{
|
||||||
|
throw new ArgumentException("Wrong wallet content");
|
||||||
|
}
|
||||||
|
|
||||||
|
var encryptedKey = wallet.Accounts[accountIndex].Key;
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(encryptedKey))
|
||||||
|
{
|
||||||
|
throw new ArgumentException("Cannot get encrypted WIF");
|
||||||
|
}
|
||||||
|
|
||||||
|
var privateKey = CryptoWallet.GetKeyFromEncodedWif(password, encryptedKey!);
|
||||||
|
var wif = privateKey.GetWIFFromPrivateKey();
|
||||||
|
|
||||||
|
return wif;
|
||||||
|
}
|
||||||
|
}
|
24
src/FrostFS.SDK.Client/Wallets/Account.cs
Normal file
24
src/FrostFS.SDK.Client/Wallets/Account.cs
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace FrostFS.SDK.Client.Wallets;
|
||||||
|
|
||||||
|
public class Account
|
||||||
|
{
|
||||||
|
[JsonPropertyName("address")]
|
||||||
|
public string? Address { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("key")]
|
||||||
|
public string? Key { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("label")]
|
||||||
|
public string? Label { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("contract")]
|
||||||
|
public Contract? Contract { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("lock")]
|
||||||
|
public bool Lock { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("isDefault")]
|
||||||
|
public bool IsDefault { get; set; }
|
||||||
|
}
|
15
src/FrostFS.SDK.Client/Wallets/Contract.cs
Normal file
15
src/FrostFS.SDK.Client/Wallets/Contract.cs
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace FrostFS.SDK.Client.Wallets;
|
||||||
|
|
||||||
|
public class Contract
|
||||||
|
{
|
||||||
|
[JsonPropertyName("script")]
|
||||||
|
public string? Script { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("parameters")]
|
||||||
|
public Parameter[]? Parameters { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("deployed")]
|
||||||
|
public bool Deployed { get; set; }
|
||||||
|
}
|
6
src/FrostFS.SDK.Client/Wallets/Extra.cs
Normal file
6
src/FrostFS.SDK.Client/Wallets/Extra.cs
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
namespace FrostFS.SDK.Client.Wallets;
|
||||||
|
|
||||||
|
public class Extra
|
||||||
|
{
|
||||||
|
public string? Tokens { get; set; }
|
||||||
|
}
|
12
src/FrostFS.SDK.Client/Wallets/Parameter.cs
Normal file
12
src/FrostFS.SDK.Client/Wallets/Parameter.cs
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace FrostFS.SDK.Client.Wallets;
|
||||||
|
|
||||||
|
public class Parameter
|
||||||
|
{
|
||||||
|
[JsonPropertyName("name")]
|
||||||
|
public string? Name { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("type")]
|
||||||
|
public string? Type { get; set; }
|
||||||
|
}
|
15
src/FrostFS.SDK.Client/Wallets/ScryptValue.cs
Normal file
15
src/FrostFS.SDK.Client/Wallets/ScryptValue.cs
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace FrostFS.SDK.Client.Wallets;
|
||||||
|
|
||||||
|
public class ScryptValue
|
||||||
|
{
|
||||||
|
[JsonPropertyName("n")]
|
||||||
|
public int N { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("r")]
|
||||||
|
public int R { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("p")]
|
||||||
|
public int P { get; set; }
|
||||||
|
}
|
18
src/FrostFS.SDK.Client/Wallets/Wallet.cs
Normal file
18
src/FrostFS.SDK.Client/Wallets/Wallet.cs
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace FrostFS.SDK.Client.Wallets;
|
||||||
|
|
||||||
|
public class Wallet
|
||||||
|
{
|
||||||
|
[JsonPropertyName("version")]
|
||||||
|
public string? Version { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("accounts")]
|
||||||
|
public Account[]? Accounts { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("scrypt")]
|
||||||
|
public ScryptValue? Scrypt { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("extra")]
|
||||||
|
public Extra? Extra { get; set; }
|
||||||
|
}
|
|
@ -32,7 +32,7 @@ public static class Base58
|
||||||
|
|
||||||
public static string Base58CheckEncode(this Span<byte> data)
|
public static string Base58CheckEncode(this Span<byte> data)
|
||||||
{
|
{
|
||||||
byte[] checksum = DataHasher.Sha256(DataHasher.Sha256(data).AsSpan()); ;
|
byte[] checksum = DataHasher.Sha256(DataHasher.Sha256(data).AsSpan());
|
||||||
Span<byte> buffer = stackalloc byte[data.Length + 4];
|
Span<byte> buffer = stackalloc byte[data.Length + 4];
|
||||||
data.CopyTo(buffer);
|
data.CopyTo(buffer);
|
||||||
|
|
||||||
|
|
|
@ -35,6 +35,7 @@
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="System.Memory" Version="4.5.5" />
|
<PackageReference Include="System.Memory" Version="4.5.5" />
|
||||||
|
<PackageReference Include="System.Text.Json" Version="7.0.4" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|
|
@ -82,6 +82,25 @@ public static class KeyExtension
|
||||||
return DataHasher.Sha256(script.AsSpan()).RIPEMD160();
|
return DataHasher.Sha256(script.AsSpan()).RIPEMD160();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static string GetWIFFromPrivateKey(this byte[] privateKey)
|
||||||
|
{
|
||||||
|
if (privateKey == null || privateKey.Length != 32)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(privateKey));
|
||||||
|
}
|
||||||
|
|
||||||
|
Span<byte> wifSpan = stackalloc byte[34];
|
||||||
|
|
||||||
|
wifSpan[0] = 0x80;
|
||||||
|
wifSpan[33] = 0x01;
|
||||||
|
|
||||||
|
privateKey.AsSpan().CopyTo(wifSpan.Slice(1));
|
||||||
|
|
||||||
|
var wif = Base58.Base58CheckEncode(wifSpan);
|
||||||
|
|
||||||
|
return wif;
|
||||||
|
}
|
||||||
|
|
||||||
private static string ToAddress(this byte[] scriptHash, byte version)
|
private static string ToAddress(this byte[] scriptHash, byte version)
|
||||||
{
|
{
|
||||||
Span<byte> data = stackalloc byte[21];
|
Span<byte> data = stackalloc byte[21];
|
||||||
|
|
98
src/FrostFS.SDK.Cryptography/WalletExtractor.cs
Normal file
98
src/FrostFS.SDK.Cryptography/WalletExtractor.cs
Normal file
|
@ -0,0 +1,98 @@
|
||||||
|
using System;
|
||||||
|
using System.Text;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
using Org.BouncyCastle.Crypto.Generators;
|
||||||
|
|
||||||
|
namespace FrostFS.SDK.Cryptography;
|
||||||
|
|
||||||
|
public static class CryptoWallet
|
||||||
|
{
|
||||||
|
private static int N = 16384;
|
||||||
|
private static int R = 8;
|
||||||
|
private static int P = 8;
|
||||||
|
|
||||||
|
private enum CipherAction
|
||||||
|
{
|
||||||
|
Encrypt,
|
||||||
|
Decrypt
|
||||||
|
}
|
||||||
|
|
||||||
|
public static byte[] GetKeyFromEncodedWif(byte[] password, string encryptedWIF)
|
||||||
|
{
|
||||||
|
var nep2Data = Base58.Base58CheckDecode(encryptedWIF);
|
||||||
|
|
||||||
|
if (nep2Data.Length == 39 && nep2Data[0] == 1 && nep2Data[1] == 66 && nep2Data[2] == 224)
|
||||||
|
{
|
||||||
|
var addressHash = nep2Data.AsSpan(3, 4).ToArray();
|
||||||
|
var derivedKey = SCrypt.Generate(password, addressHash, N, R, P, 64);
|
||||||
|
var derivedKeyHalf1 = derivedKey.Take(32).ToArray();
|
||||||
|
var derivedKeyHalf2 = derivedKey.Skip(32).ToArray();
|
||||||
|
var encrypted = nep2Data.AsSpan(7, 32).ToArray();
|
||||||
|
var decrypted = Aes(encrypted, derivedKeyHalf2, CipherAction.Decrypt);
|
||||||
|
var plainPrivateKey = XorRange(decrypted, derivedKeyHalf1, 0, decrypted.Length);
|
||||||
|
|
||||||
|
return plainPrivateKey;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw new ArgumentException("Not valid NEP2 prefix.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string EncryptWif(string plainText, ECDsa key)
|
||||||
|
{
|
||||||
|
var addressHash = GetAddressHash(key);
|
||||||
|
var derivedKey = SCrypt.Generate(Encoding.UTF8.GetBytes(plainText), addressHash, N, R, P, 64);
|
||||||
|
var derivedHalf1 = derivedKey.Take(32).ToArray();
|
||||||
|
var derivedHalf2 = derivedKey.Skip(32).ToArray();
|
||||||
|
var encryptedHalf1 = Aes(XorRange(key.PrivateKey(), derivedHalf1, 0, 16), derivedHalf2, CipherAction.Encrypt);
|
||||||
|
var encryptedHalf2 = Aes(XorRange(key.PrivateKey(), derivedHalf1, 16, 32), derivedHalf2, CipherAction.Encrypt);
|
||||||
|
var prefixes = new byte[] { 1, 66, 224 };
|
||||||
|
var concatenation = ArrayHelper.Concat([prefixes, addressHash, encryptedHalf1, encryptedHalf2]);
|
||||||
|
|
||||||
|
return Base58.Base58CheckEncode(concatenation);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static byte[] GetAddressHash(ECDsa key)
|
||||||
|
{
|
||||||
|
string address = key.PublicKey().PublicKeyToAddress();
|
||||||
|
|
||||||
|
using SHA256 sha256 = SHA256.Create();
|
||||||
|
byte[] addressHashed = sha256.ComputeHash(Encoding.UTF8.GetBytes(address));
|
||||||
|
return [.. addressHashed.Take(4)];
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte[] XorRange(byte[] arr1, byte[] arr2, int from, int length)
|
||||||
|
{
|
||||||
|
byte[] result = new byte[length];
|
||||||
|
|
||||||
|
var j = 0;
|
||||||
|
for (var i = from; i < length; ++i)
|
||||||
|
{
|
||||||
|
result[j++] = (byte)(arr1[i] ^ arr2[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte[] Aes(byte[] data, byte[] key, CipherAction action)
|
||||||
|
{
|
||||||
|
using var aes = System.Security.Cryptography.Aes.Create();
|
||||||
|
aes.Key = key;
|
||||||
|
aes.Mode = CipherMode.ECB;
|
||||||
|
aes.Padding = PaddingMode.None;
|
||||||
|
|
||||||
|
if (action == CipherAction.Encrypt)
|
||||||
|
{
|
||||||
|
return aes.CreateEncryptor().TransformFinalBlock(data, 0, data.Length);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action == CipherAction.Decrypt)
|
||||||
|
{
|
||||||
|
return aes.CreateDecryptor().TransformFinalBlock(data, 0, data.Length);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new ArgumentException("Wrong cippher action", nameof(action));
|
||||||
|
}
|
||||||
|
}
|
30
src/FrostFS.SDK.Tests/TestData/wallet.json
Normal file
30
src/FrostFS.SDK.Tests/TestData/wallet.json
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
{
|
||||||
|
"version": "1.0",
|
||||||
|
"accounts": [
|
||||||
|
{
|
||||||
|
"address": "NWeByJPgNC97F83hTUnSbnZSBKaFvk5HNw",
|
||||||
|
"key": "6PYVCcS2yp89JpcfR61FGhdhhzyYjSErNedmpZErnybNTxUZMRdhzJLrek",
|
||||||
|
"label": "",
|
||||||
|
"contract": {
|
||||||
|
"script": "DCEDJOdiiPy5ABANAYAqFO+XfMpFrQc1YSMERt8Us0TIWLZBVuezJw==",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "parameter0",
|
||||||
|
"type": "Signature"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"deployed": false
|
||||||
|
},
|
||||||
|
"lock": false,
|
||||||
|
"isDefault": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"scrypt": {
|
||||||
|
"n": 16384,
|
||||||
|
"r": 8,
|
||||||
|
"p": 8
|
||||||
|
},
|
||||||
|
"extra": {
|
||||||
|
"Tokens": null
|
||||||
|
}
|
||||||
|
}
|
25
src/FrostFS.SDK.Tests/Unit/WalletTests.cs
Normal file
25
src/FrostFS.SDK.Tests/Unit/WalletTests.cs
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using System.Text;
|
||||||
|
using FrostFS.SDK.Client;
|
||||||
|
|
||||||
|
namespace FrostFS.SDK.Tests.Unit;
|
||||||
|
|
||||||
|
[SuppressMessage("Reliability", "CA2007:Consider calling ConfigureAwait on the awaited task", Justification = "Default Value is correct for tests")]
|
||||||
|
public class WalletTest : SessionTestsBase
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void TestWallet()
|
||||||
|
{
|
||||||
|
var password = Encoding.UTF8.GetBytes("");
|
||||||
|
|
||||||
|
var d = Directory.GetCurrentDirectory();
|
||||||
|
var path = ".\\..\\..\\..\\TestData\\wallet.json";
|
||||||
|
Assert.True(File.Exists(path));
|
||||||
|
|
||||||
|
var content = File.ReadAllText(path);
|
||||||
|
|
||||||
|
var wif = WalletTools.GetWifFromWallet(content, password);
|
||||||
|
|
||||||
|
Assert.Equal("KzPXA6669m2pf18XmUdoR8MnP1pi1PMmefiFujStVFnv7WR5SRmK", wif);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Add table
Reference in a new issue