Skip to content

Instantly share code, notes, and snippets.

@RileyGe
Created December 22, 2020 08:24
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save RileyGe/55f588e0f29faf5465204f12bd450a0a to your computer and use it in GitHub Desktop.
Save RileyGe/55f588e0f29faf5465204f12bd450a0a to your computer and use it in GitHub Desktop.
Algo-Wallet密钥文件本地存储流程演示--只为流程演示,不保证可以运行
using System;
using System.Text;
using System.Collections.Generic;
using Org.BouncyCastle.Security;
using Org.BouncyCastle.Crypto;
using Org.BouncyCastle.Crypto.Generators;
using Org.BouncyCastle.Crypto.Engines;
using Org.BouncyCastle.Crypto.Modes;
using Org.BouncyCastle.Crypto.Parameters;
namespace privatkey_scrypt
{
public class CryptoUtils
{
private const int KEY_BIT_SIZE = 256;
private const int MAC_BIT_SIZE = 128;
public static byte[] DecryptAesGcm(byte[] key, byte[] nonce, byte[] cipherText, byte[] tag)
{
List<byte> msgList = new List<byte>(cipherText);
msgList.AddRange(tag);
byte[] message = msgList.ToArray();
if (key == null || key.Length != KEY_BIT_SIZE / 8)
throw new ArgumentException(String.Format("Key needs to be {0} bit!", KEY_BIT_SIZE), "key");
if (message == null || message.Length == 0)
throw new ArgumentException("Message required!", "message");
var cipher = new GcmBlockCipher(new AesEngine());
var parameters = new AeadParameters(new KeyParameter(key), MAC_BIT_SIZE, nonce);
cipher.Init(false, parameters);
var plainText = new byte[cipher.GetOutputSize(message.Length)];
try
{
var len = cipher.ProcessBytes(message, 0, message.Length, plainText, 0);
cipher.DoFinal(plainText, len);
}
catch (InvalidCipherTextException)
{
return null;
}
return plainText;
}
public static byte[] EncryptAesGcm(byte[] key, byte[] nonce, byte[] plaintext)
{
if (key == null || key.Length != KEY_BIT_SIZE / 8)
throw new ArgumentException(String.Format("Key needs to be {0} bit!", KEY_BIT_SIZE), "key");
var cipher = new GcmBlockCipher(new AesEngine());
var parameters = new AeadParameters(new KeyParameter(key), MAC_BIT_SIZE, nonce);
cipher.Init(true, parameters);
var cipherText = new byte[cipher.GetOutputSize(plaintext.Length)];
var len = cipher.ProcessBytes(plaintext, 0, plaintext.Length, cipherText, 0);
cipher.DoFinal(cipherText, len);
return cipherText;
}
public static byte[] GetCipherTextFromAesGcmResult(byte[] result)
{
if (result.Length == 16 + 32)
return result.AsSpan().Slice(0, 32).ToArray();
else
return null;
}
public static byte[] GetTagFromAesGcmResult(byte[] result)
{
if (result.Length == 16 + 32)
return result.AsSpan().Slice(32, 16).ToArray();
else
return null;
}
/// <summary>
/// Generate a random 16 bits salt
/// </summary>
/// <returns>16 bits salt</returns>
public static byte[] GenerateRandomSalt()
{
var salt = new byte[16];
new SecureRandom().NextBytes(salt);
return salt;
}
/// <summary>
/// Use Scrypt to generate 48 bytes hash.
/// Scrypt is used instead of Bscrypt because Bscrypt cannot work when importing the wallet
/// </summary>
/// <param name="salt">salt</param>
/// <param name="pwd">password</param>
/// <returns>the generated 48 bytes hash</returns>
public static byte[] GenerateHash(byte[] salt, string pwd)
{
if (salt.Length != 16)
return null;
var password = Encoding.UTF8.GetBytes(pwd);
const int SCryptN = 262144;
return SCrypt.Generate(password, salt, SCryptN, 8, 1, 48);
}
/// <summary>
/// Use the first 16 bytes to check the entered password is correct
/// </summary>
/// <param name="key">a 48 bytes, usually generated by Scrypt</param>
/// <returns>the first 16 bytes</returns>
public static byte[] GetCheckSalt(byte[] key)
{
if (key.Length != 48)
return null;
var key_list = new List<byte>(key);
key_list.RemoveRange(16, 32);
return key_list.ToArray();
}
/// <summary>
/// Use the last 32 bytes as the Master-Key of algorand account
/// </summary>
/// <param name="key">a 48 bytes, usually generated by Scrypt</param>
/// <returns>the last 32 bytes</returns>
public static byte[] GetMasterKey(byte[] key)
{
if (key.Length != 48)
return null;
var key_list = new List<byte>(key);
key_list.RemoveRange(0, 16);
return key_list.ToArray();
}
}
}
using System;
using System.Linq;
using System.Text;
namespace privatkey_scrypt
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Hello World!");
}
/// <summary>
/// 加密过程
/// </summary>
/// <param name="accountPassword">密码</param>
/// <param name="masterKey">私钥:32位</param>
static void scrypt(string accountPassword, byte[] masterKey)
{
var salt = CryptoUtils.GenerateRandomSalt(); //生成16位的随机salt,需要存储于本地
var scryptedKey = CryptoUtils.GenerateHash(salt, accountPassword); //生成48位二进制
var checkSalt = CryptoUtils.GetCheckSalt(scryptedKey); //前16位用于存储本地,后续用于较验密码
//GetMasterKey只是获取了48位二进制的后32位,此32位并不是MasterKey,是用于AESGCM加密的Key
//由于历史原因起名为GetMasterKey,为了与Algo-Wallet一致此处也用此名字
var aesgcmKey = CryptoUtils.GetMasterKey(scryptedKey);
//如果需要秘钥文件可以在不同的程序间迁移,则用固定值
//如果需要秘钥文件在不同的程序间不可迁移,请使用
//Org.BouncyCastle.Security.SecureRandom类生成随机nonce
var nonce = Encoding.UTF8.GetBytes("algo--wallet");
// aesgcmCipherBytes为48位二进制,存储本地
var aesgcmCipherBytes = CryptoUtils.EncryptAesGcm(aesgcmKey, nonce, masterKey);
}
/// <summary>
/// 解密过程
/// </summary>
/// <param name="accountPassword">密码</param>
/// <param name="salt">存储于本地的16位salt</param>
/// <param name="checkSalt">存储于本地的16位二进制</param>
/// <param name="aesgcmCipherBytes">存储于本地的48位二进制</param>
static void descrypt(string accountPassword, byte[] salt, byte[] checkSalt, byte[] aesgcmCipherBytes)
{
var scryptedKey = CryptoUtils.GenerateHash(salt, accountPassword); //生成48位二进制
var calcedCheckSalt = CryptoUtils.GetCheckSalt(scryptedKey); //前16位用于存储本地,后续用于较验密码
if(!Enumerable.SequenceEqual(checkSalt, calcedCheckSalt)) return; //密码错误
var nonce = Encoding.UTF8.GetBytes("algo--wallet");
//解密得到masterKey
var masterKey = CryptoUtils.DecryptAesGcm(CryptoUtils.GetMasterKey(scryptedKey), nonce,
CryptoUtils.GetCipherTextFromAesGcmResult(aesgcmCipherBytes),
CryptoUtils.GetTagFromAesGcmResult(aesgcmCipherBytes));
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment