6 Commits

Author SHA1 Message Date
f371f3da49 main: Attempted to add seed phrase decryption 2023-06-16 15:36:58 +10:00
a024ce7afc readme: Added installation dependencies
- Added instructions for installing .net desktop
- Added instructions for installing Node, NPM, and git
2023-06-16 11:20:48 +10:00
88ee50f4a6 main: Dependencies install links 2023-06-15 22:56:35 +10:00
23cbace1ea package: Increased version 2023-06-15 22:28:51 +10:00
6894e9c079 main: Fixed installation of hsd 2023-06-15 22:21:12 +10:00
95d0498672 main: cleaned up code 2023-06-15 22:01:32 +10:00
5 changed files with 181 additions and 278 deletions

View File

@@ -12,7 +12,7 @@
<PackageIcon>HSDBatcher.png</PackageIcon>
<RepositoryUrl>https://github.com/Nathanwoodburn/FireWallet</RepositoryUrl>
<RepositoryType>git</RepositoryType>
<Version>3.0</Version>
<Version>3.1</Version>
</PropertyGroup>
<ItemGroup>
@@ -30,7 +30,6 @@
<PackageReference Include="DnsClient" Version="1.7.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="QRCoder" Version="1.4.3" />
<PackageReference Include="Yubico.YubiKey" Version="1.7.0" />
</ItemGroup>
<ItemGroup>

View File

@@ -95,7 +95,6 @@ namespace FireWallet
textBoxReceiveAddress = new TextBox();
labelReceive1 = new Label();
panelDomains = new Panel();
labelDomainSort = new Label();
comboBoxDomainSort = new ComboBox();
buttonExportDomains = new Button();
groupBoxDomains = new GroupBox();
@@ -104,7 +103,6 @@ namespace FireWallet
textBoxDomainSearch = new TextBox();
panelSettings = new Panel();
groupBoxSettingsWallet = new GroupBox();
buttonSettingsYubikey = new Button();
buttonSettingsRescan = new Button();
buttonSeed = new Button();
groupBoxSettingsMisc = new GroupBox();
@@ -125,6 +123,7 @@ namespace FireWallet
textBoxExAddr = new TextBox();
labelSettings4 = new Label();
textBoxExTX = new TextBox();
labelDomainSort = new Label();
statusStripmain.SuspendLayout();
panelaccount.SuspendLayout();
groupBoxaccount.SuspendLayout();
@@ -242,7 +241,7 @@ namespace FireWallet
//
panelaccount.BackColor = Color.Transparent;
panelaccount.Controls.Add(groupBoxaccount);
panelaccount.Location = new Point(132, 30);
panelaccount.Location = new Point(1082, 211);
panelaccount.Name = "panelaccount";
panelaccount.Size = new Size(1074, 642);
panelaccount.TabIndex = 1;
@@ -575,7 +574,7 @@ namespace FireWallet
panelSend.Controls.Add(labelSendingTo);
panelSend.Controls.Add(labelSendPrompt);
panelSend.Controls.Add(labelHIPArrow);
panelSend.Location = new Point(880, 441);
panelSend.Location = new Point(138, 33);
panelSend.Name = "panelSend";
panelSend.Size = new Size(974, 521);
panelSend.TabIndex = 2;
@@ -793,22 +792,12 @@ namespace FireWallet
panelDomains.Controls.Add(groupBoxDomains);
panelDomains.Controls.Add(labelDomainSearch);
panelDomains.Controls.Add(textBoxDomainSearch);
panelDomains.Location = new Point(861, 364);
panelDomains.Location = new Point(120, 48);
panelDomains.Name = "panelDomains";
panelDomains.Size = new Size(920, 536);
panelDomains.TabIndex = 18;
panelDomains.Visible = false;
//
// labelDomainSort
//
labelDomainSort.AutoSize = true;
labelDomainSort.Font = new Font("Segoe UI", 12F, FontStyle.Regular, GraphicsUnit.Point);
labelDomainSort.Location = new Point(638, 15);
labelDomainSort.Name = "labelDomainSort";
labelDomainSort.Size = new Size(42, 21);
labelDomainSort.TabIndex = 12;
labelDomainSort.Text = "Sort:";
//
// comboBoxDomainSort
//
comboBoxDomainSort.DropDownStyle = ComboBoxStyle.DropDownList;
@@ -882,7 +871,7 @@ namespace FireWallet
panelSettings.Controls.Add(buttonSettingsSave);
panelSettings.Controls.Add(groupBoxSettingsExplorer);
panelSettings.Font = new Font("Segoe UI", 12F, FontStyle.Regular, GraphicsUnit.Point);
panelSettings.Location = new Point(848, 306);
panelSettings.Location = new Point(1065, 51);
panelSettings.Name = "panelSettings";
panelSettings.Size = new Size(930, 550);
panelSettings.TabIndex = 19;
@@ -890,7 +879,6 @@ namespace FireWallet
//
// groupBoxSettingsWallet
//
groupBoxSettingsWallet.Controls.Add(buttonSettingsYubikey);
groupBoxSettingsWallet.Controls.Add(buttonSettingsRescan);
groupBoxSettingsWallet.Controls.Add(buttonSeed);
groupBoxSettingsWallet.Location = new Point(507, 16);
@@ -900,17 +888,6 @@ namespace FireWallet
groupBoxSettingsWallet.TabStop = false;
groupBoxSettingsWallet.Text = "Wallet Controls";
//
// buttonSettingsYubikey
//
buttonSettingsYubikey.FlatStyle = FlatStyle.Flat;
buttonSettingsYubikey.Location = new Point(6, 133);
buttonSettingsYubikey.Name = "buttonSettingsYubikey";
buttonSettingsYubikey.Size = new Size(98, 50);
buttonSettingsYubikey.TabIndex = 9;
buttonSettingsYubikey.Text = "YubiKey";
buttonSettingsYubikey.UseVisualStyleBackColor = true;
buttonSettingsYubikey.Click += buttonSettingsYubikey_Click;
//
// buttonSettingsRescan
//
buttonSettingsRescan.FlatStyle = FlatStyle.Flat;
@@ -1104,15 +1081,25 @@ namespace FireWallet
textBoxExTX.Size = new Size(307, 29);
textBoxExTX.TabIndex = 1;
//
// labelDomainSort
//
labelDomainSort.AutoSize = true;
labelDomainSort.Font = new Font("Segoe UI", 12F, FontStyle.Regular, GraphicsUnit.Point);
labelDomainSort.Location = new Point(638, 15);
labelDomainSort.Name = "labelDomainSort";
labelDomainSort.Size = new Size(42, 21);
labelDomainSort.TabIndex = 12;
labelDomainSort.Text = "Sort:";
//
// MainForm
//
AutoScaleDimensions = new SizeF(7F, 15F);
AutoScaleMode = AutoScaleMode.Font;
ClientSize = new Size(1152, 575);
Controls.Add(panelaccount);
Controls.Add(panelSettings);
Controls.Add(panelDomains);
Controls.Add(panelSend);
Controls.Add(panelSettings);
Controls.Add(panelaccount);
Controls.Add(panelPortfolio);
Controls.Add(panelRecieve);
Controls.Add(panelNav);
@@ -1247,6 +1234,5 @@ namespace FireWallet
private Label labelSendingHIPAddress;
private ComboBox comboBoxDomainSort;
private Label labelDomainSort;
private Button buttonSettingsYubikey;
}
}

View File

@@ -7,14 +7,14 @@ using QRCoder;
using System.Text.RegularExpressions;
using System.Security.Cryptography;
using System.Text;
using System.Security.Policy;
using System.Windows.Forms;
using System.Net;
using DnsClient;
using DnsClient.Protocol;
using System.Security.Cryptography.X509Certificates;
using System.Net.Security;
// Used to use Yubikey to login
using Yubico.YubiKey;
using Yubico.YubiKey.Piv;
using System.Numerics;
namespace FireWallet
{
@@ -222,6 +222,28 @@ namespace FireWallet
return false;
}
}
} else
{
if (!Directory.Exists(dir + "hsd"))
{
NotifyForm Notifyinstall = new NotifyForm("Installing hsd\nThis may take a few minutes\nDo not close FireWallet", false);
Notifyinstall.Show();
// Wait for the notification to show
await Task.Delay(1000);
string repositoryUrl = "https://github.com/handshake-org/hsd.git";
string destinationPath = dir + "hsd";
CloneRepository(repositoryUrl, destinationPath);
Notifyinstall.CloseNotification();
Notifyinstall.Dispose();
}
if (!Directory.Exists(dir + "hsd\\node_modules"))
{
AddLog("HSD install failed");
this.Close();
return false;
}
}
@@ -665,6 +687,7 @@ namespace FireWallet
toolStripStatusLabelLedger.Text = "Cold Wallet";
toolStripStatusLabelLedger.Visible = true;
buttonRevealAll.Visible = false;
buttonSeed.Enabled = false;
}
else
@@ -672,6 +695,7 @@ namespace FireWallet
watchOnly = false;
toolStripStatusLabelLedger.Visible = false;
buttonRevealAll.Visible = true;
buttonSeed.Enabled = true;
}
@@ -701,27 +725,7 @@ namespace FireWallet
}
account = comboBoxaccount.Text;
if (textBoxaccountpassword.Text == "")
{
if (File.Exists(dir + account + ".yubikey"))
{
// Check if yubikey is plugged in
var devices = YubiKeyDevice.FindAll();
if (devices.Count() > 0)
{
// Get key from yubikey
password = YubiUnlock();
}
}
} else password = textBoxaccountpassword.Text;
password = textBoxaccountpassword.Text;
bool loggedin = await Login();
if (loggedin)
{
@@ -863,7 +867,6 @@ namespace FireWallet
{
AddLog("Post Error: " + ex.Message);
AddLog(await resp.Content.ReadAsStringAsync());
AddLog("Content: " + content);
return "Error";
}
@@ -1437,27 +1440,6 @@ namespace FireWallet
return hex.ToString();
}
private static string GetHash(HashAlgorithm hashAlgorithm, byte[] input)
{
// Convert the input string to a byte array and compute the hash.
byte[] data = hashAlgorithm.ComputeHash(input);
// Create a new Stringbuilder to collect the bytes
// and create a string.
var sBuilder = new StringBuilder();
// Loop through each byte of the hashed data
// and format each one as a hexadecimal string.
for (int i = 0; i < data.Length; i++)
{
sBuilder.Append(data[i].ToString("x2"));
}
// Return the hexadecimal string.
return sBuilder.ToString();
}
private void textBoxSendingAmount_Leave(object sender, EventArgs e)
{
decimal amount = 0;
@@ -1670,7 +1652,7 @@ namespace FireWallet
if (!outputInstalled.Contains("git version"))
{
AddLog("Git is not installed");
NotifyForm notifyForm = new NotifyForm("Git is not installed\nPlease install it to install HSD dependencies");
NotifyForm notifyForm = new NotifyForm("Git is not installed\nPlease install it to install HSD dependencies","Install", "https://git-scm.com/download/win");
notifyForm.ShowDialog();
notifyForm.Dispose();
this.Close();
@@ -1692,7 +1674,7 @@ namespace FireWallet
if (!outputInstalled.Contains("v"))
{
AddLog("Node is not installed");
NotifyForm notifyForm = new NotifyForm("Node is not installed\nPlease install it to install HSD dependencies");
NotifyForm notifyForm = new NotifyForm("Node is not installed\nPlease install it to install HSD dependencies","Install", "https://nodejs.org/en/download");
notifyForm.ShowDialog();
notifyForm.Dispose();
this.Close();
@@ -1718,7 +1700,7 @@ namespace FireWallet
{
AddLog("NPM is not installed");
AddLog(outputInstalled);
NotifyForm notifyForm = new NotifyForm("NPM is not installed\nPlease install it to install HSD dependencies");
NotifyForm notifyForm = new NotifyForm("NPM is not installed\nPlease install it to install HSD dependencies","Install", "https://docs.npmjs.com/downloading-and-installing-node-js-and-npm");
notifyForm.ShowDialog();
notifyForm.Dispose();
this.Close();
@@ -2052,6 +2034,43 @@ namespace FireWallet
domainSearch = Regex.Replace(textBoxDomainSearch.Text, "[^a-zA-Z0-9-_]", "");
textBoxDomainSearch.Text = domainSearch;
}
private void export_Click(object sender, EventArgs e)
{
SaveFileDialog saveFileDialog = new SaveFileDialog();
saveFileDialog.Filter = "CSV file (*.csv)|*.csv";
saveFileDialog.Title = "Export";
if (saveFileDialog.ShowDialog() == DialogResult.OK)
{
StreamWriter sw = new StreamWriter(saveFileDialog.FileName);
foreach (string domain in DomainsRenewable)
{
if (domain == null) break;
sw.WriteLine(domain);
}
sw.Dispose();
}
}
private void buttonRenewAll_Click(object sender, EventArgs e)
{
if (DomainsRenewable == null)
{
NotifyForm notifyForm = new NotifyForm("No renewable domains found");
notifyForm.ShowDialog();
notifyForm.Dispose();
return;
}
foreach (string domain in DomainsRenewable)
{
if (domain == null) break;
AddBatch(domain, "RENEW");
}
}
private void comboBoxDomainSort_DropDownClosed(object sender, EventArgs e)
{
UpdateDomains();
}
#endregion
#region Batching
@@ -2158,6 +2177,8 @@ namespace FireWallet
try
{
AddLog("Decrypting seed...");
AddLog(resp.ToString());
string iv = resp["iv"].ToString();
string ciphertext = resp["ciphertext"].ToString();
string tmpn = resp["n"].ToString();
@@ -2166,9 +2187,26 @@ namespace FireWallet
int n = int.Parse(tmpn);
int p = int.Parse(tmpp);
int r = int.Parse(tmpr);
int iterations = n;
byte[] decripted = await Decrypt_Seed(algorithm, ciphertext, iv, n,r,p);
// This is returning garbled text
AddLog("Seed decrypted");
string phrase = Encoding.UTF8.GetString(decripted);
AddLog("Your seed phrase is:\n" + phrase);
phrase = Encoding.ASCII.GetString(decripted);
AddLog("Your seed phrase is:\n" + phrase);
phrase = Encoding.Unicode.GetString(decripted);
AddLog("Your seed phrase is:\n" + phrase);
}
catch (Exception ex)
@@ -2183,7 +2221,71 @@ namespace FireWallet
}
}
private async Task<byte[]> Decrypt_Seed(string algorithm, string ciphertext, string iv, int n,int r, int p)
{
byte[] salt = Encoding.ASCII.GetBytes("hsd");
using (AesManaged aes = new AesManaged())
{
aes.Key = DeriveKey(algorithm, password, salt, n, r, p);
aes.IV = HexStringToByteArray(iv);
aes.Mode = CipherMode.CBC;
aes.Padding = PaddingMode.None;
byte[] cipher = HexStringToByteArray(ciphertext);
if (cipher.Length % 16 != 0)
{
AddLog("Invalid cipher length");
return null;
}
using (ICryptoTransform decryptor = aes.CreateDecryptor())
{
byte[] decrypted = decryptor.TransformFinalBlock(cipher, 0, cipher.Length);
return decrypted;
}
}
}
static byte[] HexStringToByteArray(string hex)
{
int numberChars = hex.Length / 2;
byte[] bytes = new byte[numberChars];
for (int i = 0; i < numberChars; i++)
{
bytes[i] = Convert.ToByte(hex.Substring(i * 2, 2), 16);
}
return bytes;
}
byte[] DeriveKey(string algorithm, string passphrase, byte[] salt, int n, int r, int p)
{
byte[] passwordBytes = Encoding.UTF8.GetBytes(passphrase);
switch (algorithm)
{
case "pbkdf2":
return Pbkdf2DeriveKey(passwordBytes, salt, n, 32);
case "scrypt":
return ScryptDeriveKey(passwordBytes, salt, n, r, p, 32);
default:
throw new Exception($"Unknown algorithm: {algorithm}.");
}
}
static byte[] Pbkdf2DeriveKey(byte[] password, byte[] salt, int iterations, int derivedKeyLength)
{
using (Rfc2898DeriveBytes pbkdf2 = new Rfc2898DeriveBytes(password, salt, iterations))
{
return pbkdf2.GetBytes(derivedKeyLength);
}
}
static byte[] ScryptDeriveKey(byte[] password, byte[] salt, int costParameterN, int costParameterR, int costParameterP, int derivedKeyLength)
{
using (var rfc2898 = new Rfc2898DeriveBytes(password, salt, costParameterN, HashAlgorithmName.SHA256))
{
return rfc2898.GetBytes(derivedKeyLength);
}
}
private async void Rescan_Click(object sender, EventArgs e)
{
string content = "{\"height\": 0}";
@@ -2199,39 +2301,7 @@ namespace FireWallet
}
#endregion
private void export_Click(object sender, EventArgs e)
{
SaveFileDialog saveFileDialog = new SaveFileDialog();
saveFileDialog.Filter = "CSV file (*.csv)|*.csv";
saveFileDialog.Title = "Export";
if (saveFileDialog.ShowDialog() == DialogResult.OK)
{
StreamWriter sw = new StreamWriter(saveFileDialog.FileName);
foreach (string domain in DomainsRenewable)
{
if (domain == null) break;
sw.WriteLine(domain);
}
sw.Dispose();
}
}
private void buttonRenewAll_Click(object sender, EventArgs e)
{
if (DomainsRenewable == null)
{
NotifyForm notifyForm = new NotifyForm("No renewable domains found");
notifyForm.ShowDialog();
notifyForm.Dispose();
return;
}
foreach (string domain in DomainsRenewable)
{
if (domain == null) break;
AddBatch(domain, "RENEW");
}
}
#region Help Menu
private void githubToolStripMenuItem_Click(object sender, EventArgs e)
{
// Open the GitHub page
@@ -2262,166 +2332,6 @@ namespace FireWallet
};
Process.Start(psi);
}
private void comboBoxDomainSort_DropDownClosed(object sender, EventArgs e)
{
UpdateDomains();
}
#region yubikey
static bool PinSubmitter(KeyEntryData pin)
{
string s = "123456";
var s_b = Encoding.UTF8.GetBytes(s);
pin.SubmitValue(s_b);
return true;
}
private void buttonSettingsYubikey_Click(object sender, EventArgs e)
{
if (password.Length < 0)
{
return;
}
NotifyForm notifyForm = new NotifyForm("Insert Yubikey\nThis will use your yubikey to encrypt your account password.");
notifyForm.ShowDialog();
notifyForm.Dispose();
var devices = YubiKeyDevice.FindAll();
if (devices.Count() != 1)
{
NotifyForm notifyForm2 = new NotifyForm("Please insert your yubikey and try again.");
notifyForm2.ShowDialog();
notifyForm2.Dispose();
return;
}
NotifyForm yubiLoadingForm = new NotifyForm("Encrypting. . .", false);
yubiLoadingForm.Show();
// Wait for the form to load
Application.DoEvents();
try
{
//Assumes there is exactly one yubikey connected and it has a RSA2048 certificate in slot 9d
//PIV PIN is assumed to be 123456
var ykDevice = devices.First();
PivSession piv = new(ykDevice);
piv.KeyCollector += PinSubmitter;
piv.VerifyPin();
var slot = PivSlot.KeyManagement;
X509Certificate2 cert = piv.GetCertificate(slot);
if (cert.SignatureAlgorithm.FriendlyName != "sha256RSA")
throw new CryptographicException("Certificate must be RSA with SHA256");
var publicKey = cert.GetRSAPublicKey() ?? throw new CryptographicException("Couldn't get public key from certificate.");
Aes aesFirst = Aes.Create();
var encryptedKey = publicKey.Encrypt(aesFirst.Key, RSAEncryptionPadding.Pkcs1);
var decryptedKey = piv.Decrypt(slot, encryptedKey);
//MessageBox.Show($"aesFirst.Key.Length: {aesFirst.Key.Length}");
//MessageBox.Show($"encryptedKey.Length: {encryptedKey.Length}");
//MessageBox.Show($"decryptedKey.Length: {decryptedKey.Length}");
// split the message into blocks of 128 bytes
string message = password;
int blockSize = 128;
int blockCount = (int)Math.Ceiling((double)message.Length / blockSize);
string[] strings = new string[blockCount];
FileStream sw = new FileStream(dir + account + ".yubikey", FileMode.Create, FileAccess.Write);
for (int i = 0; i < blockCount; i++)
{
int size = Math.Min(blockSize, message.Length - i * blockSize);
strings[i] = message.Substring(i * blockSize, size);
byte[] bytes = Encoding.ASCII.GetBytes(strings[i]);
var encryptedBytes = publicKey.Encrypt(bytes, RSAEncryptionPadding.Pkcs1);
sw.Write(encryptedBytes, 0, encryptedBytes.Length);
}
sw.Close();
sw.Dispose();
}
catch (Exception ex)
{
AddLog(ex.Message);
}
yubiLoadingForm.CloseNotification();
}
private string YubiUnlock()
{
NotifyForm yubiLoadingForm = new NotifyForm("Decrypting. . .", false);
yubiLoadingForm.Show();
// Wait for the form to load
Application.DoEvents();
try
{
//Assumes there is exactly one yubikey connected and it has a RSA2048 certificate in slot 9d
//PIV PIN is assumed to be 123456
var devices = YubiKeyDevice.FindAll();
var ykDevice = devices.First();
PivSession piv = new(ykDevice);
piv.KeyCollector += PinSubmitter;
piv.VerifyPin();
var slot = PivSlot.KeyManagement;
X509Certificate2 cert = piv.GetCertificate(slot);
if (cert.SignatureAlgorithm.FriendlyName != "sha256RSA")
throw new CryptographicException("Certificate must be RSA with SHA256");
var publicKey = cert.GetRSAPublicKey() ?? throw new CryptographicException("Couldn't get public key from certificate.");
Aes aesFirst = Aes.Create();
var encryptedKey = publicKey.Encrypt(aesFirst.Key, RSAEncryptionPadding.Pkcs1);
var decryptedKey = piv.Decrypt(slot, encryptedKey);
byte[] input = File.ReadAllBytes(dir + account + ".yubikey");
// decrypt the input
int blockSize = 256;
int blockCount = (int)Math.Ceiling((double)input.Length / blockSize);
byte[][] blocks = new byte[blockCount][];
byte[] decripted = new byte[blockCount * blockSize];
string output = "";
for (int i = 0; i < blockCount; i++)
{
int size = Math.Min(blockSize, input.Length - i * blockSize);
blocks[i] = new byte[size];
Array.Copy(input, i * blockSize, blocks[i], 0, size);
var paddedDecryptedBytes = piv.Decrypt(slot, blocks[i]);
byte[] decryptedBytes;
bool couldParse = Yubico.YubiKey.Cryptography.RsaFormat.TryParsePkcs1Decrypt(paddedDecryptedBytes, out decryptedBytes);
Array.Copy(decryptedBytes, 0, decripted, i * blockSize, decryptedBytes.Length);
output += Encoding.ASCII.GetString(decryptedBytes);
}
yubiLoadingForm.CloseNotification();
return output;
}
catch (Exception ex)
{
AddLog(ex.Message);
yubiLoadingForm.CloseNotification();
return "";
}
}
#endregion
}
}

View File

@@ -224,15 +224,15 @@
{
"Name" = "8:Microsoft Visual Studio"
"ProductName" = "8:FireWallet"
"ProductCode" = "8:{007B0A5E-57B9-4DB7-AABE-5A3631A89BEB}"
"PackageCode" = "8:{546D4209-3E58-4144-A9DC-E659A2482DD4}"
"ProductCode" = "8:{460D8F86-4FE9-4547-9B17-7E01ACBF9194}"
"PackageCode" = "8:{A6678F97-9CE8-4005-82AC-AB2D09A9DA27}"
"UpgradeCode" = "8:{0C86F725-6B01-4173-AA05-3F0EDF481362}"
"AspNetVersion" = "8:"
"RestartWWWService" = "11:FALSE"
"RemovePreviousVersions" = "11:TRUE"
"DetectNewerInstalledVersion" = "11:TRUE"
"InstallAllUsers" = "11:FALSE"
"ProductVersion" = "8:3.0"
"ProductVersion" = "8:3.1"
"Manufacturer" = "8:Nathan.Woodburn/"
"ARPHELPTELEPHONE" = "8:"
"ARPHELPLINK" = "8:https://l.woodburn.au/discord"

View File

@@ -2,6 +2,14 @@
Experimental wallet for Handshake chain
## Installation
### Dependencies
You will need .net desktop installed. You can download it from [here](https://dotnet.microsoft.com/en-us/download/dotnet/thank-you/runtime-desktop-6.0.18-windows-x64-installer).
You will also need Node, NPM, and git installed if you want to use the internal HSD or Ledger wallets.
[Git](https://git-scm.com/downloads)
[Node](https://nodejs.org/en/download/)
[NPM](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm)
### From Releases
You can install the latest release from [here](https://github.com/Nathanwoodburn/FireWallet/releases/).