From d3ff987de1f4d43754cf4603e3f0d50dede9fd72 Mon Sep 17 00:00:00 2001 From: fluorine Date: Fri, 25 Aug 2017 23:19:58 -0400 Subject: [PATCH 1/5] Init --- README.md | 63 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..cc13eba --- /dev/null +++ b/README.md @@ -0,0 +1,63 @@ +This tool can be used to create parts of **Cool Wallets**, and later to recover them. + +# Usage +To **generate** parts for a Private Key, the `-g` argument must be provided, followed by the number of parts required to recover the Private Key, then the total of parts to be generated, and finally the Private Key, between double quotation marks. All arguments must be separated by spaces. + +In this example, a 3 / 9 wallet is created. The tool will generate a total of 9 parts, but later only any 3 different generated parts are required to recover the Private Key. + +``` +tool -g 3 9 "ThisIsAStrangePrivateKey" +``` + +Generated parts will be shown in console, using the Short Notation: +``` +Parts generated: + - 1|3|9|af1-1-622105eff2e34a57e6af256898e508c8f823dc3492ef48a0 + - 1|3|9|561-2-3466980b69178b3c940d455586baa56243fa109af06eaaac + - 1|3|9|6b1-3-91969eabcbfb1c448e246f279480724adec79fbba280cfeb + - 1|3|9|1f1-4-c1e06dd2d13a92845ef4e128325679a85aaba33df61317aa + - 1|3|9|be1-5-64106b7273d605fc44ddcb5a206cae80c7962c1ca4fd72ff + - 1|3|9|7f1-6-3257f696e822c497367fab673e33032a7c4fe0b2c67c90d7 + - 1|3|9|b41-7-97a7f0364ace53ef2c5681152c09d402e1726f939492f584 + - 1|3|9|581-8-4ecabe239e72cc4d75f239cd7ac1de41d3806f9cf7118a66 + - 1|3|9|db1-9-eb3ab8833c9e5b356fdb13bf68fb09694ebde0bda5ffef6f +``` +Note: Even if you use the same parameters to generate the wallet, the generated parts are different and will not be compatible with the parts in this example. If mixed, the resulting Private Key will be unpredictable. + +You can also save the output in a file. + +``` +tool -g 3 9 "ThisIsAStrangePrivateKey" > output.txt +``` + +Keep in mind that no guarantee is provided with this tool. Use at your own risk. + +To **recover** the Private Key, the tool must have the flag argument of `-r`, followed by a list of parts required to recover the Private Key. Parts must be provided in double quotation marks. All arguments (including the parts) must be separated by two or more spaces. + +``` +tool -r "1|3|9|db1-9-eb3ab8833c9e5b356fdb13bf68fb09694ebde0bda5ffef6f" "1|3|9|1f1-4-c1e06dd2d13a92845ef4e128325679a85aaba33df61317aa" "1|3|9|561-2-3466980b69178b3c940d455586baa56243fa109af06eaaac" +``` + +If the parts are unit, uncorrupted and compatible with each other, the Private Key will be shown in console: +``` +Recovered Private Key: + ThisIsAStrangePrivateKey +``` + +To get some **help**, you can use the `-h` argument. +``` +tool -h +``` + +# What is a Cool Wallet? +The theoretical definition of a Cool Wallet is _a compound wallet of m total parts, in a way that a quantity under n parts is not enough to recover the private key, but on or over n parts is, assuming that n is less or equal to m._ Another name for a Cool Wallet is a [Multi-signature wallet](https://en.bitcoin.it/wiki/Multisignature), but often its use case is more technical or internal than that of a Cool Wallet. + +Maybe an example is a better way to explain what a Cool Wallet is. + +Suppose Mark wants to mitigate the risk of storing the 12 BTC he saved growing up. He first considered a Cold Wallet, but he also likes to travel, so the media can be lost or stolen. He still wants to have his funds in hand for any emergency. He realizes that a Cold Wallet is not ideal for his purposes, so he considered a Cool Wallet instead. He generated a simple Public-Private key pair and provided the Private Key to a Cold Wallet tool to generate the parts of a Cool Wallet. He generated 6 parts in a way that only 3 different parts are required to recover the Private Key and get all his funds, which means he created a 3 / 6 Cool Wallet. + +Mark stored one wallet part in his mother's house (1), he gave another to a trusted friend (2), he stored another one in his laptop (3, remember it is not a Cold Wallet), another one in his physical wallet as a QR code (4), he stored another one in his USB drive (5) and the last one is stored as an image in his cell phone (6). Mark may lost his wallet, phone and wife (just kidding), but he will still able to recover the funds from the parts left and create a new Cool Wallet to move the funds from the previous one. A very clever hacker could had access to the Cool Wallet's part stored in Mark's laptop, but the hacker was unable to get any funds without more different parts to combine. + +Mark never lost control of his wallet, since only he knew and had access to all his Cool Wallet's parts. He also mitigated the risk of losing his wallet, since only a fraction of the generated parts are required to recover the Private Key. + +Therefore, Cool Wallets are called as such because they may or may not be strictly used as Cold Wallets, but they are a lot safer than online wallets or exchanges as storage of cryptocurrencies. From 6f946727c9648c0ee95802b10e7518df0f9dca24 Mon Sep 17 00:00:00 2001 From: fluorine Date: Fri, 25 Aug 2017 23:20:44 -0400 Subject: [PATCH 2/5] Cool Wallet Core --- CoolWallet.Core/CoolWallet.Core.csproj | 72 +++++++++ .../CoolWallet/IShortNotationFormat.cs | 32 ++++ CoolWallet.Core/CoolWallet/IValidable.cs | 24 +++ CoolWallet.Core/CoolWallet/IWallet.cs | 17 +++ CoolWallet.Core/CoolWallet/IWalletPart.cs | 15 ++ .../CoolWallet/IWalletSignature.cs | 28 ++++ CoolWallet.Core/CoolWallet/Wallet.cs | 142 +++++++++++++++++ CoolWallet.Core/CoolWallet/WalletPart.cs | 97 ++++++++++++ CoolWallet.Core/CoolWallet/WalletSignature.cs | 96 ++++++++++++ CoolWallet.Core/Properties/AssemblyInfo.cs | 36 +++++ .../Properties/Strings.Designer.cs | 135 ++++++++++++++++ CoolWallet.Core/Properties/Strings.resx | 144 ++++++++++++++++++ CoolWallet.Core/packages.config | 4 + 13 files changed, 842 insertions(+) create mode 100644 CoolWallet.Core/CoolWallet.Core.csproj create mode 100644 CoolWallet.Core/CoolWallet/IShortNotationFormat.cs create mode 100644 CoolWallet.Core/CoolWallet/IValidable.cs create mode 100644 CoolWallet.Core/CoolWallet/IWallet.cs create mode 100644 CoolWallet.Core/CoolWallet/IWalletPart.cs create mode 100644 CoolWallet.Core/CoolWallet/IWalletSignature.cs create mode 100644 CoolWallet.Core/CoolWallet/Wallet.cs create mode 100644 CoolWallet.Core/CoolWallet/WalletPart.cs create mode 100644 CoolWallet.Core/CoolWallet/WalletSignature.cs create mode 100644 CoolWallet.Core/Properties/AssemblyInfo.cs create mode 100644 CoolWallet.Core/Properties/Strings.Designer.cs create mode 100644 CoolWallet.Core/Properties/Strings.resx create mode 100644 CoolWallet.Core/packages.config diff --git a/CoolWallet.Core/CoolWallet.Core.csproj b/CoolWallet.Core/CoolWallet.Core.csproj new file mode 100644 index 0000000..35f140b --- /dev/null +++ b/CoolWallet.Core/CoolWallet.Core.csproj @@ -0,0 +1,72 @@ + + + + + Debug + AnyCPU + {3326E219-8092-4949-9C3B-DBE619F606D3} + Library + Properties + CoolWallet.Core + CoolWallet.Core + v4.6.1 + 512 + + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + ..\packages\Moserware.SecretSplitter.0.12.0.0\lib\net40\Moserware.SecretSplitter.dll + + + + + + + + + + + + + True + True + Strings.resx + + + + + + + + + + + + + + + + + PublicResXFileCodeGenerator + Strings.Designer.cs + + + + \ No newline at end of file diff --git a/CoolWallet.Core/CoolWallet/IShortNotationFormat.cs b/CoolWallet.Core/CoolWallet/IShortNotationFormat.cs new file mode 100644 index 0000000..3f2cabc --- /dev/null +++ b/CoolWallet.Core/CoolWallet/IShortNotationFormat.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace CoolWallet.Core +{ + public interface IShortNotationFormat + { + /// + /// Get short notation to represent an object + /// with the least text possible. Useful to + /// encode data in QR codes. + /// + /// A short notation of the object. + /// Null if object does not have a valid state. + /// + string GetShortNotation(); + + /// + /// Interpret a given short notation for the + /// object to fill key properties. + /// + /// Short notation text. + /// + /// True if short notation was parsed successfully. + /// False otherwise. + /// + bool InterpretShortNotation(string shortNotation); + } +} diff --git a/CoolWallet.Core/CoolWallet/IValidable.cs b/CoolWallet.Core/CoolWallet/IValidable.cs new file mode 100644 index 0000000..66c3d72 --- /dev/null +++ b/CoolWallet.Core/CoolWallet/IValidable.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace CoolWallet.Core +{ + public interface IValidable + { + /// + /// Check if signature is valid. + /// + /// True if valid. False otherwise + bool IsValid(); + + /// + /// Check if signature is valid. + /// + /// Message explaining why signature is invalid. + /// True if valid. False otherwise + bool IsValid(out string message); + } +} diff --git a/CoolWallet.Core/CoolWallet/IWallet.cs b/CoolWallet.Core/CoolWallet/IWallet.cs new file mode 100644 index 0000000..43cd97b --- /dev/null +++ b/CoolWallet.Core/CoolWallet/IWallet.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace CoolWallet.Core +{ + public interface IWallet : IValidable, IEquatable + { + IWalletSignature Signature { get; } + + string PrivateKey { get; } + + IEnumerable Parts { get; } + } +} diff --git a/CoolWallet.Core/CoolWallet/IWalletPart.cs b/CoolWallet.Core/CoolWallet/IWalletPart.cs new file mode 100644 index 0000000..5f474e5 --- /dev/null +++ b/CoolWallet.Core/CoolWallet/IWalletPart.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace CoolWallet.Core +{ + public interface IWalletPart : IShortNotationFormat, IValidable, IEquatable + { + IWalletSignature Signature { get; set; } + + string Data { get; set; } + } +} diff --git a/CoolWallet.Core/CoolWallet/IWalletSignature.cs b/CoolWallet.Core/CoolWallet/IWalletSignature.cs new file mode 100644 index 0000000..4384683 --- /dev/null +++ b/CoolWallet.Core/CoolWallet/IWalletSignature.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace CoolWallet.Core +{ + public interface IWalletSignature : IShortNotationFormat, IValidable, IEquatable + { + /// + /// Version used to generate the parts of the Cold Wallet. + /// + int Version { get; } + + /// + /// Numbers of parts required to recompose a Wallet (Private Key). + /// It is a numerator. + /// + int PartsThreshold { get; } + + /// + /// Total of parts generated from the Wallet (Privete Key). + /// It is a denominator. + /// + int PartsTotal { get; } + } +} diff --git a/CoolWallet.Core/CoolWallet/Wallet.cs b/CoolWallet.Core/CoolWallet/Wallet.cs new file mode 100644 index 0000000..22b8fee --- /dev/null +++ b/CoolWallet.Core/CoolWallet/Wallet.cs @@ -0,0 +1,142 @@ +using Moserware.Security.Cryptography; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace CoolWallet.Core +{ + /// + /// Full representation of a Shared Cold Wallet. + /// + public class Wallet : IWallet + { + public IWalletSignature Signature { get; } + + public string PrivateKey { get; private set; } + + public IEnumerable Parts { get; private set; } + + private Wallet(IWalletSignature signature) + { + // Validate signature + if(signature == null) + { + throw new ArgumentNullException(nameof(signature)); + } + + if (!signature.IsValid(out string message)) + { + throw new ArgumentException(message); + } + + // Add data + Signature = signature; + } + + /// + /// Constructor to create a new wallet using a private key. + /// + /// Signature of the wallet. + /// Private key to divide + public Wallet(IWalletSignature signature, string privateKey) : this(signature) + { + // Validate private key + if (string.IsNullOrWhiteSpace(privateKey)) { + throw new ArgumentException($"Argument '{nameof(privateKey)}' is null or white space."); + } + + PrivateKey = privateKey; + + // Produce wallet parts + ProduceWalletParts(); + } + + public Wallet(IEnumerable parts) : this(parts?.FirstOrDefault()?.Signature) + { + if(parts == null || !parts.Any()) + { + throw new ArgumentNullException(nameof(parts)); + } + + var signature = parts.First().Signature; + + // Check if all parts have the same signatures + if(!parts.All(p => p.Signature.Equals(signature))) + { + throw new ArgumentException(Properties.Strings.SharesHaveDifferentSignatures); + } + + // Get only unique parts. Discard shares with duplicate data. + var _parts = parts.GroupBy(i => i.Data).Select(i => i.First()); + + // Check if there are enough parts + if(_parts.Count() < signature.PartsThreshold) + { + throw new ArgumentException(Properties.Strings.NotEnoughShares); + } + + // Validate parts + foreach(var part in _parts) + { + if(part.IsValid(out string message)) continue; + + throw new ArgumentException("Invalid part: " + message); + } + + Parts = _parts; + + // Produce private key + var shares = _parts + .Take(signature.PartsThreshold) + .Select(i => i.Data) + .ToArray(); + + try + { + PrivateKey = SecretCombiner.Combine(shares).RecoveredTextString; + } catch (InvalidChecksumShareException) + { + throw new ArgumentException(Properties.Strings.ChecksumExceptionSharesCorrupt); + } + } + + public bool Equals(IWallet other) + { + throw new NotImplementedException(); + } + + public bool IsValid() + { + return Signature.IsValid() && (PrivateKey != null || (Parts != null && Parts.Any())); + } + + public bool IsValid(out string message) + { + throw new NotImplementedException(); + } + + private void ProduceWalletParts() + { + if (PrivateKey == null || Signature == null) return; + + var _parts = new List(Signature.PartsTotal); + + // Generate Parts from the Private Key + var shares = SecretSplitter + .SplitMessage(PrivateKey, Signature.PartsThreshold, Signature.PartsTotal); + + foreach(var share in shares) + { + _parts.Add(new WalletPart() + { + Signature = Signature, + Data = share + }); + } + + Parts = _parts; + } + } +} diff --git a/CoolWallet.Core/CoolWallet/WalletPart.cs b/CoolWallet.Core/CoolWallet/WalletPart.cs new file mode 100644 index 0000000..25f0b40 --- /dev/null +++ b/CoolWallet.Core/CoolWallet/WalletPart.cs @@ -0,0 +1,97 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace CoolWallet.Core +{ + /// + /// Wallet parts used to recompose a cold wallet. + /// + public class WalletPart : IWalletPart + { + public IWalletSignature Signature { get; set; } + + public string Data { get; set; } + + public WalletPart() { } + + public WalletPart(string shortNotation) + { + InterpretShortNotation(shortNotation); + } + + public bool Equals(IWalletPart other) + { + return Signature.Equals(other.Signature) && Data.Equals(other.Data); + } + + public string GetShortNotation() + { + if (!IsValid()) return null; + + return $"{Signature.GetShortNotation()}|{Data}"; + } + + public bool InterpretShortNotation(string rawShortNotation) + { + if (rawShortNotation == null) return false; + + var shortNotation = rawShortNotation.Trim().Trim('|'); + + var delimiterOccurences = shortNotation.Count(i => i == '|'); + + if (delimiterOccurences != 3) return false; + + var lastIndex = shortNotation.LastIndexOf('|'); + + if (lastIndex < 0) return false; + + // Set notation + var signatureNotation = shortNotation.Substring(0, lastIndex).Trim('|'); + + var signature = new WalletSignature(); + var isValidSignature = signature.InterpretShortNotation(signatureNotation); + + // Set Data + var data = shortNotation.Substring(lastIndex, shortNotation.Length - lastIndex).Trim().Trim('|'); + var isValidData = data.Length > 0; + + // Set valid data to wallet part + if (!isValidSignature || !isValidData) return false; + + Signature = signature; + Data = data; + + return true; + } + + public bool IsValid() + { + return IsValid(out string message); + } + + public bool IsValid(out string message) + { + if(Signature == null) + { + message = Properties.Strings.SignatureIsNull; + return false; + } + + if (!Signature.IsValid(out message)) + { + return false; + } + + if(string.IsNullOrWhiteSpace(Data)) + { + message = Properties.Strings.DataIsNullOrEmpty; + return false; + } + + return true; + } + } +} diff --git a/CoolWallet.Core/CoolWallet/WalletSignature.cs b/CoolWallet.Core/CoolWallet/WalletSignature.cs new file mode 100644 index 0000000..59a76d4 --- /dev/null +++ b/CoolWallet.Core/CoolWallet/WalletSignature.cs @@ -0,0 +1,96 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using CoolWallet.Core.Properties; + +namespace CoolWallet.Core +{ + public class WalletSignature : IWalletSignature + { + public WalletSignature() { } + + public WalletSignature(string shortNotation) + { + InterpretShortNotation(shortNotation); + } + + public int Version { get; set; } = 1; + + public int PartsThreshold { get; set; } + + public int PartsTotal { get; set; } + + public bool IsValid(out string message) + { + message = null; + + if(PartsTotal < 1) + { + message = Strings.SharesTotalCannotBeLessThanOne; + return false; + } + + if(PartsThreshold < 1) + { + message = Strings.SharesThresholdCannotBeLessThanOne; + return false; + } + + if(PartsThreshold > PartsTotal) + { + message = Strings.SharesThresholdCannotBeLessThanSharesTotal; + return false; + } + + return true; + } + + public bool IsValid() + { + return IsValid(out string message); + } + + public bool Equals(IWalletSignature other) + { + return Version == other.Version + && PartsThreshold == other.PartsThreshold + && PartsTotal == other.PartsTotal; + } + + public string GetShortNotation() + { + if (!IsValid()) return null; + + return $"{Version}|{PartsThreshold}|{PartsTotal}"; + } + + public bool InterpretShortNotation(string shortNotation) + { + // Validate + if (shortNotation == null) return false; + + var tokens = shortNotation.Split('|'); + + if (tokens.Length != 3) return false; + + // Parse fields + var validParse = int.TryParse(tokens[0], out int version); + validParse = int.TryParse(tokens[1], out int threshold) && validParse; + validParse = int.TryParse(tokens[2], out int total) && validParse; + + if (!validParse) return false; + + // Fill the object's fields + // and validate its state. + Version = version; + PartsThreshold = threshold; + PartsTotal = total; + + if (!IsValid()) return false; + + return true; + } + } +} diff --git a/CoolWallet.Core/Properties/AssemblyInfo.cs b/CoolWallet.Core/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..b9bba23 --- /dev/null +++ b/CoolWallet.Core/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("SharedColdWallet")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("SharedColdWallet")] +[assembly: AssemblyCopyright("Copyright © 2017")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("3326e219-8092-4949-9c3b-dbe619f606d3")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/CoolWallet.Core/Properties/Strings.Designer.cs b/CoolWallet.Core/Properties/Strings.Designer.cs new file mode 100644 index 0000000..f6dc925 --- /dev/null +++ b/CoolWallet.Core/Properties/Strings.Designer.cs @@ -0,0 +1,135 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace CoolWallet.Core.Properties { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + public class Strings { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Strings() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + public static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("CoolWallet.Core.Properties.Strings", typeof(Strings).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + public static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to Checksum Exception: Provided parts are corrupt or incomplete.. + /// + public static string ChecksumExceptionSharesCorrupt { + get { + return ResourceManager.GetString("ChecksumExceptionSharesCorrupt", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Data should not be null or empty.. + /// + public static string DataIsNullOrEmpty { + get { + return ResourceManager.GetString("DataIsNullOrEmpty", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Not enough unique parts to get the private key.. + /// + public static string NotEnoughShares { + get { + return ResourceManager.GetString("NotEnoughShares", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to All provided parts must have the same signature.. + /// + public static string SharesHaveDifferentSignatures { + get { + return ResourceManager.GetString("SharesHaveDifferentSignatures", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The threshold number of parts cannot be less than one.. + /// + public static string SharesThresholdCannotBeLessThanOne { + get { + return ResourceManager.GetString("SharesThresholdCannotBeLessThanOne", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The threshold number of parts cannot be less than the total number of shares.. + /// + public static string SharesThresholdCannotBeLessThanSharesTotal { + get { + return ResourceManager.GetString("SharesThresholdCannotBeLessThanSharesTotal", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The total number of parts cannot be less than one.. + /// + public static string SharesTotalCannotBeLessThanOne { + get { + return ResourceManager.GetString("SharesTotalCannotBeLessThanOne", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Signature cannot be null.. + /// + public static string SignatureIsNull { + get { + return ResourceManager.GetString("SignatureIsNull", resourceCulture); + } + } + } +} diff --git a/CoolWallet.Core/Properties/Strings.resx b/CoolWallet.Core/Properties/Strings.resx new file mode 100644 index 0000000..c389a64 --- /dev/null +++ b/CoolWallet.Core/Properties/Strings.resx @@ -0,0 +1,144 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Checksum Exception: Provided parts are corrupt or incomplete. + + + Data should not be null or empty. + + + Not enough unique parts to get the private key. + + + All provided parts must have the same signature. + + + The threshold number of parts cannot be less than one. + + + The threshold number of parts cannot be less than the total number of shares. + + + The total number of parts cannot be less than one. + + + Signature cannot be null. + + \ No newline at end of file diff --git a/CoolWallet.Core/packages.config b/CoolWallet.Core/packages.config new file mode 100644 index 0000000..e48e8a8 --- /dev/null +++ b/CoolWallet.Core/packages.config @@ -0,0 +1,4 @@ + + + + \ No newline at end of file From ea10080484a5693e7ab26e71f089c82d8b40d0e1 Mon Sep 17 00:00:00 2001 From: fluorine Date: Fri, 25 Aug 2017 23:21:23 -0400 Subject: [PATCH 3/5] Cool Wallet Tests --- CoolWallet.Tests/CoolWallet.Tests.csproj | 82 +++ CoolWallet.Tests/Properties/AssemblyInfo.cs | 36 ++ .../SecretSplitter/SecretSplitterSamples.cs | 155 ++++++ .../SecretSplitter/SecretSplitterTests.cs | 12 + CoolWallet.Tests/WalletPartTests.cs | 360 +++++++++++++ CoolWallet.Tests/WalletSignatureTests.cs | 265 ++++++++++ CoolWallet.Tests/WalletTests.cs | 480 ++++++++++++++++++ CoolWallet.Tests/packages.config | 8 + 8 files changed, 1398 insertions(+) create mode 100644 CoolWallet.Tests/CoolWallet.Tests.csproj create mode 100644 CoolWallet.Tests/Properties/AssemblyInfo.cs create mode 100644 CoolWallet.Tests/SecretSplitter/SecretSplitterSamples.cs create mode 100644 CoolWallet.Tests/SecretSplitter/SecretSplitterTests.cs create mode 100644 CoolWallet.Tests/WalletPartTests.cs create mode 100644 CoolWallet.Tests/WalletSignatureTests.cs create mode 100644 CoolWallet.Tests/WalletTests.cs create mode 100644 CoolWallet.Tests/packages.config diff --git a/CoolWallet.Tests/CoolWallet.Tests.csproj b/CoolWallet.Tests/CoolWallet.Tests.csproj new file mode 100644 index 0000000..72e79a3 --- /dev/null +++ b/CoolWallet.Tests/CoolWallet.Tests.csproj @@ -0,0 +1,82 @@ + + + + + + Debug + AnyCPU + {8181DF65-9485-42A8-BF3A-D135192F49EF} + Library + Properties + CoolWallet.Tests + CoolWallet.Tests + v4.6.1 + 512 + + + + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + ..\packages\Moserware.SecretSplitter.0.12.0.0\lib\net40\Moserware.SecretSplitter.dll + + + ..\packages\Newtonsoft.Json.10.0.3\lib\net45\Newtonsoft.Json.dll + + + ..\packages\NUnit.3.7.1\lib\net45\nunit.framework.dll + + + + + + + + + + + + + + + + + + + + + + + + + + + {3326e219-8092-4949-9c3b-dbe619f606d3} + CoolWallet.Core + + + + + + This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + + + \ No newline at end of file diff --git a/CoolWallet.Tests/Properties/AssemblyInfo.cs b/CoolWallet.Tests/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..c968276 --- /dev/null +++ b/CoolWallet.Tests/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("SharedColdWallet.Tests")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("SharedColdWallet.Tests")] +[assembly: AssemblyCopyright("Copyright © 2017")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("8181df65-9485-42a8-bf3a-d135192f49ef")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/CoolWallet.Tests/SecretSplitter/SecretSplitterSamples.cs b/CoolWallet.Tests/SecretSplitter/SecretSplitterSamples.cs new file mode 100644 index 0000000..bf56bd2 --- /dev/null +++ b/CoolWallet.Tests/SecretSplitter/SecretSplitterSamples.cs @@ -0,0 +1,155 @@ +using Moserware.Security.Cryptography; +using System; +using System.Diagnostics; +using System.IO; +using System.Linq; + +namespace CoolWallet.Tests { + public static class SecretSplitterSamples { + public static void RunSamples() { + // Let's run a few examples. Simply call this method from your project. + // Feel free to follow along in the debugger. + // It's important to note that this sample file shows several different scenarios. + // Pick the one that best matches your needs and delete the others + + SplitSimpleMessage(); + SplitFile(); + + // For more details on the theory behind this program, see + // http://www.moserware.com/2011/11/life-death-and-splitting-secrets.html + } + + private static void SplitSimpleMessage() { + // In this example, we take a very simple message (a string) and split it directly. + // You can do this for messages below ~1250 characters. However, keep in mind + // that the size of the message is directly proportional to the size of *each* share/split. + + const string secretMessage = "Hello World!"; + + // The threshold is exactly how many shares need to be combined to reconstruct the secret. + // You must have exactly this many: no more, no less + const int threshold = 3; + + // The total shares is the number of shares to generate. You can generate as many as you'd like. + // Any combination of the threshold (3 in this example) can be used to reconstruct the secret; + const int totalShares = 5; + var shares = SecretSplitter.SplitMessage(secretMessage, threshold, totalShares); + + // Note that every time you run this example you'll get different shares. That's because there is + // a randomized component to the share generation process. + + Debug.Assert(shares.Length == 5); + + // Notice that *any* combination of three shares works to reconstruct the secret message + Debug.Assert(SecretCombiner.Combine(new[] { shares[0], shares[1], shares[2] }).RecoveredTextString == secretMessage); + Debug.Assert(SecretCombiner.Combine(new[] { shares[0], shares[1], shares[3] }).RecoveredTextString == secretMessage); + Debug.Assert(SecretCombiner.Combine(new[] { shares[0], shares[1], shares[4] }).RecoveredTextString == secretMessage); + Debug.Assert(SecretCombiner.Combine(new[] { shares[0], shares[2], shares[3] }).RecoveredTextString == secretMessage); + Debug.Assert(SecretCombiner.Combine(new[] { shares[0], shares[2], shares[4] }).RecoveredTextString == secretMessage); + Debug.Assert(SecretCombiner.Combine(new[] { shares[0], shares[3], shares[4] }).RecoveredTextString == secretMessage); + Debug.Assert(SecretCombiner.Combine(new[] { shares[1], shares[2], shares[3] }).RecoveredTextString == secretMessage); + Debug.Assert(SecretCombiner.Combine(new[] { shares[1], shares[2], shares[4] }).RecoveredTextString == secretMessage); + Debug.Assert(SecretCombiner.Combine(new[] { shares[1], shares[3], shares[4] }).RecoveredTextString == secretMessage); + Debug.Assert(SecretCombiner.Combine(new[] { shares[2], shares[3], shares[4] }).RecoveredTextString == secretMessage); + + // However, having less shares doesn't work + Debug.Assert(SecretCombiner.Combine(shares.Take(threshold - 1)).RecoveredTextString != secretMessage); + + // Having more shares doesn't work either + Debug.Assert(SecretCombiner.Combine(shares.Take(threshold + 1)).RecoveredTextString != secretMessage); + } + + private static void SplitFile() { + // If your message is above 1K, you should use the hybrid approach of splitting the master secret + // (which is the encryption key) and then use that to encrypt the file using OpenPGP and AES. + + // Here's an example of how to do this hybrid approach: + + // First, get a master secret of whatever size you want. 128 bits is plenty big and is a nice + // balance of share sizes and security. However, to be fun, let's be super paranoid and go with + // 256 bits (at the cost of bigger shares!) + var masterPassword = HexadecimalPasswordGenerator.GeneratePasswordOfBitSize(bitSize: 256); + var masterPasswordBytes = SecretEncoder.ParseHexString(masterPassword); + + // As mentioned above, the threshold is the total number of shares that need to come together + // to unlock the secret. + const int threshold = 3; + + // Now we create a class to help us encrypt everything else: + var splitSecret = SecretSplitter.SplitFile(masterPasswordBytes, threshold); + + // We can generate as many shares as we'd like knowing that 3 of them need to come together + // to reconstruct the secret. + const int totalShares = 5; + var shares = splitSecret.GetShares(totalShares); + + // The textual representation is what you'd typically distribute: + var sharesText = shares.Select(s => s.ToString()).ToList(); + + // Remember that the shares are just mechanisms to distribute the master password. You still + // need to distribute the encrypted file + + // Normally, you'd probably use the simpler method of + // splitSecret.EncryptFile(inputPath, outputPath); + + // But this sample will use the more generic stream based version to keep everything in memory + + // Let's get a spot to store the encrypted output + Stream encryptedStream; + + // First, let's make up a sample message of the numbers 1-1000 + // As mentioned before, normally you'd just use a simple file name with EncryptFile or an + // existing stream: + + // The OpenPGP format stores the name of the file, so we provide that here. It can be whatever + // you want + const string fileNameInsideEncryptedContainer = "numbers.txt"; + + using (var inputStream = new MemoryStream()) + using (var streamWriter = new StreamWriter(inputStream)) { + // Generate the numbers 1..1000 each on a single line + for (int i = 1; i <= 1000; i++) { + streamWriter.WriteLine(i); + } + + // we're done writing + streamWriter.Flush(); + + // Note that the size is bigger than 1250 bytes (the limit of splitting the message itself): + Debug.Assert(inputStream.Length > 1250); + + // Now we can use the input. Reset its position to the start of the stream: + inputStream.Position = 0; + + // Finally, encrypt the file. Save it with the filename metadata: + encryptedStream = splitSecret.Encrypt(inputStream, fileNameInsideEncryptedContainer); + } + + // We can save the contents of outputStream wherever we'd like. It's encrypted. + + // Let's go ahead and decrypt it. + + // First, take the threshold number of shares to recover the master secret: + var combinedSecret = SecretCombiner.Combine(sharesText.Take(threshold)); + + // This metadata is present inside the encrypted file + string decryptedFileName; + DateTime decryptedFileDateTime; + + using (var decryptedStream = combinedSecret.Decrypt(encryptedStream, out decryptedFileName, out decryptedFileDateTime)) + using (var decryptedStreamReader = new StreamReader(decryptedStream)) { + // For fun, verify the decrypted file is what we expect: + + Debug.Assert(decryptedFileName == fileNameInsideEncryptedContainer); + for (int expectedNumber = 1; expectedNumber <= 1000; expectedNumber++) { + var currentLineText = decryptedStreamReader.ReadLine(); + var currentLineNumber = Int32.Parse(currentLineText); + Debug.Assert(currentLineNumber == expectedNumber); + } + + // Nothing left: + Debug.Assert(decryptedStreamReader.EndOfStream); + } + } + } +} diff --git a/CoolWallet.Tests/SecretSplitter/SecretSplitterTests.cs b/CoolWallet.Tests/SecretSplitter/SecretSplitterTests.cs new file mode 100644 index 0000000..6ac3f02 --- /dev/null +++ b/CoolWallet.Tests/SecretSplitter/SecretSplitterTests.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace CoolWallet.Tests +{ + class SecretSplitterTests + { + } +} diff --git a/CoolWallet.Tests/WalletPartTests.cs b/CoolWallet.Tests/WalletPartTests.cs new file mode 100644 index 0000000..1511508 --- /dev/null +++ b/CoolWallet.Tests/WalletPartTests.cs @@ -0,0 +1,360 @@ +using NUnit.Framework; +using CoolWallet.Core; +using CoolWallet.Core.Properties; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace CoolWallet.Tests +{ + [TestFixture] + public class WalletPartTests + { + #region Equals + + [Test] + public void Equals_SameDataSameSignature_True() + { + var walletPartA = new WalletPart() + { + Signature = new WalletSignature("1|2|4"), + Data = "0aac471a-813f-49f8-9f87-1559bd462e93" + }; + + var walletPartB = new WalletPart() + { + Signature = new WalletSignature("1|2|4"), + Data = "0aac471a-813f-49f8-9f87-1559bd462e93" + }; + + Assert.That(walletPartA, Is.EqualTo(walletPartB)); + } + + [Test] + public void Equals_DifferentDataAndDifferentSignature_False() + { + var walletPartA = new WalletPart() + { + Signature = new WalletSignature("1|2|4"), + Data = "0aac471a-813f-49f8-9f87-1559bd462e93" + }; + + var walletPartB = new WalletPart() + { + Signature = new WalletSignature("1|4|8"), + Data = "11baa341-49a7-48c7-b390-9b2f38d855d0" + }; + + Assert.That(walletPartA, Is.Not.EqualTo(walletPartB)); + } + + [Test] + public void Equals_DifferentDataSameSignature_False() + { + var walletPartA = new WalletPart() + { + Signature = new WalletSignature("1|2|4"), + Data = "0aac471a-813f-49f8-9f87-1559bd462e93" + }; + + var walletPartB = new WalletPart() + { + Signature = new WalletSignature("1|2|4"), + Data = "11baa341-49a7-48c7-b390-9b2f38d855d0" + }; + + Assert.That(walletPartA, Is.Not.EqualTo(walletPartB)); + } + + + [Test] + public void Equals_SameDataDifferentSignature_False() + { + var walletPartA = new WalletPart() + { + Signature = new WalletSignature("1|5|20"), + Data = "0aac471a-813f-49f8-9f87-1559bd462e93" + }; + + var walletPartB = new WalletPart() + { + Signature = new WalletSignature("1|2|4"), + Data = "0aac471a-813f-49f8-9f87-1559bd462e93" + }; + + Assert.That(walletPartA, Is.Not.EqualTo(walletPartB)); + } + + #endregion + + #region IsValid + + [Test] + public void IsValid_ValidWalletPart_True() + { + var part = new WalletPart() + { + Signature = new WalletSignature("1|2|3"), + Data = "0aac471a-813f-49f8-9f87-1559bd462e93" + }; + + Assert.That(part.IsValid()); + } + + [Test] + public void IsValid_NullDataInvalidSignature_False() + { + var part = new WalletPart() + { + Signature = new WalletSignature("1|10|5"), + Data = null + }; + + Assert.That(part.IsValid(), Is.False); + } + + [Test] + public void IsValid_ValidDataInvalidSignature_False() + { + var part = new WalletPart() + { + Signature = new WalletSignature("1|10|5"), + Data = "0aac471a-813f-49f8-9f87-1559bd462e93" + }; + + Assert.That(part.IsValid(), Is.False); + } + + [Test] + public void IsValid_NullDataValidSignature_False() + { + var part = new WalletPart() + { + Signature = new WalletSignature("1|5|10"), + Data = null + }; + + Assert.That(part.IsValid(), Is.False); + } + + [Test] + public void IsValid_EmptyDataValidSignature_False() + { + var part = new WalletPart() + { + Signature = new WalletSignature("1|5|10"), + Data = " " + }; + + Assert.That(part.IsValid(), Is.False); + } + + [Test] + public void IsValid_NullSignatureValidData_False() + { + var part = new WalletPart() + { + Signature = null, + Data = "0aac471a-813f-49f8-9f87-1559bd462e93" + }; + + Assert.That(part.IsValid(), Is.False); + } + + [Test] + public void IsValid_InvalidSignatureValidData_InvalidSignatureMessage() + { + + var part = new WalletPart() + { + Signature = new WalletSignature("1|10|5"), + Data = "0aac471a-813f-49f8-9f87-1559bd462e93" + }; + + var expectedMessage = Strings.SharesThresholdCannotBeLessThanSharesTotal; + + part.IsValid(out string message); + + Assert.That(message, Is.EqualTo(expectedMessage)); + } + + [Test] + public void IsValid_ValidSignatureBlankData_BlankDataMessage() + { + + var part = new WalletPart() + { + Signature = new WalletSignature("1|5|10"), + Data = "" + }; + + var expectedMessage = Strings.DataIsNullOrEmpty; + + part.IsValid(out string message); + + Assert.That(message, Is.EqualTo(expectedMessage)); + } + + [Test] + public void IsValid_NullSignatureValidData_EmptySignatureMessage() + { + var part = new WalletPart() + { + Signature = null, + Data = "0aac471a-813f-49f8-9f87-1559bd462e93" + }; + + var expectedMessage = Strings.SignatureIsNull; + + part.IsValid(out string message); + + Assert.That(message, Is.EqualTo(expectedMessage)); + } + + #endregion + + #region GetShortNotation + + [Test] + public void GetShortNotation_ValidWalletPart_ValidShortNotation() + { + var walletPart = new WalletPart() + { + Signature = new WalletSignature() + { + Version = 1, + PartsThreshold = 2, + PartsTotal = 4 + }, + + Data = "5edb71e0-ac40-40a3-a22d-70220058eba5" + }; + + var expectedShortNotation = "1|2|4|5edb71e0-ac40-40a3-a22d-70220058eba5"; + + Assert.That(walletPart.GetShortNotation(), Is.EqualTo(expectedShortNotation)); + } + + [Test] + public void GetShortNotation_InvalidSignature_NullShortNotation() + { + var walletPart = new WalletPart() + { + Signature = new WalletSignature() + { + Version = 1, + PartsThreshold = 22, + PartsTotal = 4 + }, + + Data = "5edb71e0-ac40-40a3-a22d-70220058eba5" + }; + + Assert.IsNull(walletPart.GetShortNotation()); + } + + [Test] + public void GetShortNotation_InvalidData_NullShortNotation() + { + var walletPart = new WalletPart() + { + Signature = new WalletSignature() + { + Version = 1, + PartsThreshold = 2, + PartsTotal = 4 + }, + + Data = null + }; + + Assert.IsNull(walletPart.GetShortNotation()); + } + + #endregion + + #region InterpretShortNotation + + [Test] + public void InterpretShortNotation_ValidShortNotation_ValidWalletPart() + { + var validShortNotation = "1|2|4|5edb71e0-ac40-40a3-a22d-70220058eba5"; + + var walletPart = new WalletPart(); + + var result = walletPart.InterpretShortNotation(validShortNotation); + + Assert.IsTrue(result); + Assert.That(walletPart.Signature.Version, Is.EqualTo(1)); + Assert.That(walletPart.Signature.PartsThreshold, Is.EqualTo(2)); + Assert.That(walletPart.Signature.PartsTotal, Is.EqualTo(4)); + Assert.That(walletPart.Data, Is.EqualTo("5edb71e0-ac40-40a3-a22d-70220058eba5")); + } + + [Test] + public void InterpretShortNotationByConstructor_ValidShortNotation_ValidWalletPart() + { + var validShortNotation = "1|2|4|5edb71e0-ac40-40a3-a22d-70220058eba5"; + + var walletPart = new WalletPart(validShortNotation); + + Assert.That(walletPart.Signature.Version, Is.EqualTo(1)); + Assert.That(walletPart.Signature.PartsThreshold, Is.EqualTo(2)); + Assert.That(walletPart.Signature.PartsTotal, Is.EqualTo(4)); + Assert.That(walletPart.Data, Is.EqualTo("5edb71e0-ac40-40a3-a22d-70220058eba5")); + } + + [Test] + public void InterpretShortNotation_TruncatedInvalidShortNotation_InvalidWalletPart() + { + var validShortNotation = "1|4|5edb71e0-ac40-40a3-a22d-70220058eba5"; + + var walletPart = new WalletPart(); + + var result = walletPart.InterpretShortNotation(validShortNotation); + + Assert.IsFalse(result); + } + + [Test] + public void InterpretShortNotation_NullShortNotation_InvalidWalletPart() + { + var walletPart = new WalletPart(); + + var result = walletPart.InterpretShortNotation(null); + + Assert.IsFalse(result); + } + + + [Test] + public void InterpretShortNotation_TooLongShortNotation_InvalidWalletPart() + { + var invalidShortNotation = "1|4|5edb71e0-ac40-40a3-a22d-70220058eba5|1111|2222|3333"; + + var walletPart = new WalletPart(); + + var result = walletPart.InterpretShortNotation(invalidShortNotation); + + Assert.IsFalse(result); + } + + [Test] + public void InterpretShortNotation_ValidShortNotationWithExtraDelimiters_ValidWalletPart() + { + var validShortNotation = "|1|2|4|5edb71e0-ac40-40a3-a22d-70220058eba5|"; + + var walletPart = new WalletPart(validShortNotation); + + Assert.That(walletPart.Signature.Version, Is.EqualTo(1)); + Assert.That(walletPart.Signature.PartsThreshold, Is.EqualTo(2)); + Assert.That(walletPart.Signature.PartsTotal, Is.EqualTo(4)); + Assert.That(walletPart.Data, Is.EqualTo("5edb71e0-ac40-40a3-a22d-70220058eba5")); + } + + #endregion + + } +} diff --git a/CoolWallet.Tests/WalletSignatureTests.cs b/CoolWallet.Tests/WalletSignatureTests.cs new file mode 100644 index 0000000..996f1e7 --- /dev/null +++ b/CoolWallet.Tests/WalletSignatureTests.cs @@ -0,0 +1,265 @@ +using Newtonsoft.Json; +using NUnit.Framework; +using CoolWallet.Core; +using CoolWallet.Core.Properties; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace CoolWallet.Tests +{ + [TestFixture] + public class WalletSignatureTests + { + #region IsValid + + [Test] + public void IsValid_ValidSignature_True() + { + var signature = new WalletSignature() + { + PartsThreshold = 3, + PartsTotal = 6 + }; + + var result = signature.IsValid(); + + Assert.IsTrue(result); + } + + [Test] + public void IsValid_ZeroSharesThreshold_False() + { + var expectedMessage = Strings.SharesThresholdCannotBeLessThanOne; + var signature = new WalletSignature() + { + PartsThreshold = 0, + PartsTotal = 10 + }; + + string message; + var result = signature.IsValid(out message); + + Assert.IsFalse(result); + Assert.That(message, Is.EqualTo(expectedMessage)); + } + + [Test] + public void IsValid_NegativeSharesThreshold_False() + { + var expectedMessage = Strings.SharesThresholdCannotBeLessThanOne; + var signature = new WalletSignature() + { + PartsThreshold = -6, + PartsTotal = 10 + }; + + string message; + var result = signature.IsValid(out message); + + Assert.IsFalse(result); + Assert.That(message, Is.EqualTo(expectedMessage)); + } + + [Test] + public void IsValid_ZeroSharesTotal_False() + { + var expectedMessage = Strings.SharesTotalCannotBeLessThanOne; + var signature = new WalletSignature() + { + PartsThreshold = 3, + PartsTotal = 0 + }; + + string message; + var result = signature.IsValid(out message); + + Assert.IsFalse(result); + Assert.That(message, Is.EqualTo(expectedMessage)); + } + + [Test] + public void IsValid_NegativeSharesTotal_False() + { + var expectedMessage = Strings.SharesTotalCannotBeLessThanOne; + var signature = new WalletSignature() + { + PartsThreshold = 3, + PartsTotal = -8 + }; + + string message; + var result = signature.IsValid(out message); + + Assert.IsFalse(result); + Assert.That(message, Is.EqualTo(expectedMessage)); + } + + [Test] + public void IsValid_SharesTotalLessThanSharesThreshold_False() + { + var expectedMessage = Strings.SharesThresholdCannotBeLessThanSharesTotal; + var signature = new WalletSignature() + { + PartsThreshold = 3, + PartsTotal = 2 + }; + + string message; + var result = signature.IsValid(out message); + + Assert.IsFalse(result); + Assert.That(message, Is.EqualTo(expectedMessage)); + } + +#endregion + + #region Equals + + [Test] + public void Equals_EqualSignaturesInDifferentObjects_True() + { + var signature1 = new WalletSignature() + { + Version = 1, + PartsThreshold = 2, + PartsTotal = 5 + }; + + var signature2 = new WalletSignature() + { + Version = 1, + PartsThreshold = 2, + PartsTotal = 5 + }; + + // Different objects with the same values + Assert.That(signature1, Is.EqualTo(signature2)); + Assert.IsFalse(signature1 == signature2); + + // Compare to deserialized object + var json = JsonConvert.SerializeObject(signature1); + var deserialized = JsonConvert.DeserializeObject(json); + + Assert.That(deserialized, Is.EqualTo(signature1)); + Assert.That(deserialized, Is.EqualTo(signature2)); + } + + [Test] + public void Equals_DifferentSignaturesInDifferentObjects_False() + { + var signature1 = new WalletSignature() + { + Version = 1, + PartsThreshold = 2, + PartsTotal = 5 + }; + + var signature2 = new WalletSignature() + { + Version = 1, + PartsThreshold = 2, + PartsTotal = 7 + }; + + // Different objects with the different values + Assert.That(signature1, Is.Not.EqualTo(signature2)); + Assert.IsFalse(signature1 == signature2); + } + + #endregion + + #region GetShortNotation + + [Test] + public void GetShortNotation_ValidSignature_ValidShortNotation() + { + var expectedShortNotation = "12|5|10"; + + var signature = new WalletSignature() + { + Version = 12, + PartsThreshold = 5, + PartsTotal = 10 + }; + + var shortNotation = signature.GetShortNotation(); + + Assert.That(shortNotation, Is.EqualTo(expectedShortNotation)); + } + + #endregion + + #region InterpretShortNotation + + [Test] + public void InterpretShortNotationByConstructor_ValidNotationAndData_ValidSignature() + { + var shortNotation = "1|3|10"; + + var signature = new WalletSignature(shortNotation); + + Assert.That(signature.Version, Is.EqualTo(1)); + Assert.That(signature.PartsThreshold, Is.EqualTo(3)); + Assert.That(signature.PartsTotal, Is.EqualTo(10)); + } + + [Test] + public void InterpretShortNotation_ValidNotationAndData_ValidSignature() + { + var shortNotation = "1|3|10"; + + var signature = new WalletSignature(); + signature.InterpretShortNotation(shortNotation); + + Assert.That(signature.Version, Is.EqualTo(1)); + Assert.That(signature.PartsThreshold, Is.EqualTo(3)); + Assert.That(signature.PartsTotal, Is.EqualTo(10)); + } + + [Test] + public void InterpretShortNotationByConstructor_ValidNotationInvalidData_InvalidSignature() + { + var shortNotation = "1|10|3"; + + var signature = new WalletSignature(shortNotation); + + Assert.IsFalse(signature.IsValid()); + } + + [Test] + public void InterpretShortNotation_ValidNotationInvalidData_InvalidSignature() + { + var shortNotation = "1|10|3"; + + var signature = new WalletSignature(); + var result = signature.InterpretShortNotation(shortNotation); + + Assert.IsFalse(result); + } + + [Test] + public void InterpretShortNotation_IncompleteNotation_InvalidSignature() + { + var shortNotation = "1|10"; + + var signature = new WalletSignature(); + var result = signature.InterpretShortNotation(shortNotation); + + Assert.IsFalse(result); + } + + [Test] + public void InterpretShortNotation_Null_InvalidSignature() + { + var signature = new WalletSignature(); + var result = signature.InterpretShortNotation(null); + + Assert.IsFalse(result); + } + + #endregion + } +} diff --git a/CoolWallet.Tests/WalletTests.cs b/CoolWallet.Tests/WalletTests.cs new file mode 100644 index 0000000..77ff48d --- /dev/null +++ b/CoolWallet.Tests/WalletTests.cs @@ -0,0 +1,480 @@ +using NUnit.Framework; +using CoolWallet.Core; +using CoolWallet.Core.Properties; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace CoolWallet.Tests +{ + [TestFixture] + public class WalletTests + { + #region Constructor for Private Key + + private WalletSignature ValidSignature = new WalletSignature() + { + Version = 1, + PartsThreshold = 2, + PartsTotal = 8 + }; + + [Test] + public void Constructor_ValidSignature_NoExceptionExpected() + { + Assert.DoesNotThrow(() => + { + var signature = new WalletSignature() + { + Version = 1, + PartsThreshold = 2, + PartsTotal = 4 + }; + + var part1 = new WalletPart() + { + Signature = signature, + Data = "0e1-1-5ca6e43983ccf56aa90fd6b66ea844a7c22a92dd92343888" + }; + + var part2 = new WalletPart() + { + Signature = signature, + Data = "5d1-5-c67046150ecd502cdbc15f7a67e990a646febd88c229d3ea" + }; + + var wallet1 = new Wallet(signature, "ExampleOfPrivateKey"); + var wallet2 = new Wallet(new List { part1, part2 }); + }); + } + + [Test] + public void Constructor_NullSignature_NullArgumentExceptionExpected() + { + Assert.Throws(() => + { + var wallet1 = new Wallet(null, "ExampleOfPrivateKey"); + }); + } + + [Test] + public void Constructor_InvalidSignature_ArgumentExceptionExpected() + { + Assert.Throws(() => + { + var invalidSignature = new WalletSignature() + { + Version = 1, + PartsThreshold = 8, + PartsTotal = 4 + }; + + var wallet1 = new Wallet(invalidSignature, "ExampleOfPrivateKey"); + }); + } + + [Test] + public void Constructor_ValidData_NoExceptionExpected() + { + Assert.DoesNotThrow(() => + { + var wallet = new Wallet(ValidSignature, "ExampleOfValidPrivateKey"); + }); + } + + [Test] + public void Constructor_NullData_ArgumentException() + { + Assert.Throws(() => + { + string nullData = null; + var wallet = new Wallet(ValidSignature, nullData); + }); + } + + [Test] + public void Constructor_EmptyData_ArgumentException() + { + Assert.Throws(() => + { + var wallet = new Wallet(ValidSignature, default(string)); + }); + } + + [Test] + public void Constructor_ValidInitialization_ValidPartsQuantity() + { + var signature = new WalletSignature() + { + Version = 1, + PartsThreshold = 2, + PartsTotal = 4 + }; + + var wallet = new Wallet(signature, "ExampleOfPrivateKey"); + + // Validate quantity of parts + Assert.That(wallet.Parts.Count, Is.EqualTo(signature.PartsTotal)); + } + + [Test] + public void Constructor_ValidInitialization_ValidPartsContent() + { + var signature = new WalletSignature() + { + Version = 1, + PartsThreshold = 2, + PartsTotal = 4 + }; + + var wallet = new Wallet(signature, "constant forest adore false green weave stop guy fur freeze giggle clock"); + + // Validate parts's content + Console.WriteLine("Signature: " + signature.GetShortNotation()); + foreach(var part in wallet.Parts) + { + Assert.That(part.Signature.Equals(signature)); + Assert.That(part.Data, Is.Not.Null.Or.Empty); + + Console.WriteLine("Data: " + part.Data); + } + } + + [Test] + public void Constructor_NullParts_ArgumentNullException() + { + var signature = new WalletSignature() + { + Version = 1, + PartsThreshold = 2, + PartsTotal = 4 + }; + + Assert.Throws(() => { + var wallet = new Wallet(default(List)); + }); + } + + [Test] + public void Constructor_NotEnoughParts_ArgumentExceptionExpected() + { + var signature = new WalletSignature() + { + Version = 1, + PartsThreshold = 2, + PartsTotal = 4 + }; + + var part = new WalletPart() + { + Signature = new WalletSignature(signature.GetShortNotation()), + Data = "datadatadata" + }; + + Assert.Throws(() => + { + + var wallet = new Wallet(new List() { part }); + + }, Strings.NotEnoughShares); + } + + [Test] + public void Constructor_DuplicateParts_ArgumentExceptionExpected() + { + var signature = new WalletSignature() + { + Version = 1, + PartsThreshold = 2, + PartsTotal = 4 + }; + + var partA = new WalletPart() + { + Signature = new WalletSignature(signature.GetShortNotation()), + Data = "datadatadata" + }; + + var partB = new WalletPart() + { + Signature = new WalletSignature(signature.GetShortNotation()), + Data = "datadatadata" + }; + + Assert.Throws(() => + { + var wallet = new Wallet(new List() { partA, partB }); + }); + } + + [Test] + public void Constructor_ValidPartsWithDuplicates_NoExceptionExpected() + { + var signature = new WalletSignature() + { + Version = 1, + PartsThreshold = 2, + PartsTotal = 4 + }; + + var partA = new WalletPart() + { + Signature = new WalletSignature(signature.GetShortNotation()), + Data = "0e1-1-5ca6e43983ccf56aa90fd6b66ea844a7c22a92dd92343888" + }; + + var partB = new WalletPart() + { + Signature = new WalletSignature(signature.GetShortNotation()), + Data = "631-7-8b1b1703484d828fe2a61b9c63497aa68494aa226a272657" + }; + + var partC = new WalletPart() + { + Signature = new WalletSignature(signature.GetShortNotation()), + Data = "631-7-8b1b1703484d828fe2a61b9c63497aa68494aa226a272657" + }; + + Assert.DoesNotThrow(() => + { + var wallet = new Wallet(new List() { partA, partB, partC }); + }); + } + + [Test] + public void Constructor_ValidPartsWithDuplicates_ExpectedTrueQuantity() + { + var signature = new WalletSignature() + { + Version = 1, + PartsThreshold = 2, + PartsTotal = 4 + }; + + var partA = new WalletPart() + { + Signature = new WalletSignature(signature.GetShortNotation()), + Data = "631-7-8b1b1703484d828fe2a61b9c63497aa68494aa226a272657" + }; + + var partB = new WalletPart() + { + Signature = new WalletSignature(signature.GetShortNotation()), + Data = "5d1-5-c67046150ecd502cdbc15f7a67e990a646febd88c229d3ea" + }; + + var partC = new WalletPart() + { + Signature = new WalletSignature(signature.GetShortNotation()), + Data = "5d1-5-c67046150ecd502cdbc15f7a67e990a646febd88c229d3ea" + }; + + var wallet = new Wallet(new List() { partA, partB, partC }); + + Assert.That(wallet.Parts.Count, Is.Not.EqualTo(3)); + Assert.That(wallet.Parts.Count, Is.EqualTo(2)); + } + + [Test] + public void Constructor_EnoughValidParts_NoExceptionExpected() + { + var signature = new WalletSignature() + { + Version = 1, + PartsThreshold = 2, + PartsTotal = 4 + }; + + var partA = new WalletPart() + { + Signature = new WalletSignature(signature.GetShortNotation()), + Data = "0e1-1-5ca6e43983ccf56aa90fd6b66ea844a7c22a92dd92343888" + }; + + var partB = new WalletPart() + { + Signature = new WalletSignature(signature.GetShortNotation()), + Data = "631-7-8b1b1703484d828fe2a61b9c63497aa68494aa226a272657" + }; + + Assert.DoesNotThrow(() => + { + var wallet = new Wallet(new List() { partA, partB }); + }); + } + + [Test] + public void Constructor_InvalidParts_ArgumentExceptionExpected() + { + var partA = new WalletPart() + { + Signature = new WalletSignature() + { + Version = 1, + PartsThreshold = 2, + PartsTotal = 1 + }, + Data = "datadatadata" + }; + + var partB = new WalletPart() + { + Signature = new WalletSignature() + { + Version = 1, + PartsThreshold = 2, + PartsTotal = 1 + }, + Data = "datadatadata22222" + }; + + Assert.Throws(() => + { + var wallet = new Wallet(new List() { partA, partB }); + }); + } + + [Test] + public void Constructor_EnoughPartsWithDifferentSignatures_ArgumentExceptionExpected() + { + var partA = new WalletPart() + { + Signature = new WalletSignature() + { + Version = 2, + PartsThreshold = 2, + PartsTotal = 4 + }, + + Data = "datadatadata" + }; + + var partB = new WalletPart() + { + Signature = new WalletSignature() + { + Version = 1, + PartsThreshold = 2, + PartsTotal = 4 + }, + Data = "datadatadata" + }; + + Assert.Throws(() => + { + var wallet = new Wallet(new List() { partA, partB }); + }, Strings.SharesHaveDifferentSignatures); + } + + [Test] + public void Constructor_ValidSignatureAndPrivateKey_ValidReconstructedWallet() + { + var signature1 = new WalletSignature() + { + Version = 1, + PartsThreshold = 2, + PartsTotal = 4 + }; + + var signature2 = new WalletSignature() + { + Version = 1, + PartsThreshold = 2, + PartsTotal = 4 + }; + + var privateKey = "A private key"; + + var originalWallet = new Wallet(signature1, privateKey); + + var reconstructedWallet = new Wallet(originalWallet.Parts); + + Assert.That(reconstructedWallet.PrivateKey, Is.EqualTo(reconstructedWallet.PrivateKey)); + } + + [Test] + public void Constructor_DifferentParts_RecoveredValidPrivateKey() + { + var signature = new WalletSignature() + { + Version = 1, + PartsThreshold = 2, + PartsTotal = 4 + }; + + var partA = new WalletPart() + { + Signature = new WalletSignature(signature.GetShortNotation()), + Data = "0e1-1-5ca6e43983cff56aa90fd6b66ea844a7c22a92dd92343888" // Corrupt data + }; + + var partB = new WalletPart() + { + Signature = new WalletSignature(signature.GetShortNotation()), + Data = "631-7-8b1b1703484d828fe2a61b9c63497aa68494aa226a272657" + }; + + Assert.Throws(() => + { + var wallet = new Wallet(new List() { partA, partB }); + }, Strings.ChecksumExceptionSharesCorrupt); + } + + [Test] + public void Constructor_ValidParts_ValidPrivateKey() + { + var signature = new WalletSignature() + { + Version = 1, + PartsThreshold = 2, + PartsTotal = 4 + }; + + var privateKey = "ExampleOfPrivateKey"; + var wallet = new Wallet(signature, privateKey); + + var recoveredWallet = new Wallet( + new List() + { + new WalletPart(wallet.Parts.First().GetShortNotation()), + new WalletPart(wallet.Parts.Last().GetShortNotation()), + }); + + Assert.That(recoveredWallet.PrivateKey, Is.EqualTo(privateKey)); + Assert.That(recoveredWallet.PrivateKey, Is.EqualTo(wallet.PrivateKey)); + } + + //[Test] + //public void Constructor_ValidPartsFromDifferentWallets_InvalidPrivateKey() + //{ + // var signature = new WalletSignature() + // { + // Version = 1, + // PartsThreshold = 2, + // PartsTotal = 4 + // }; + + // var privateKey1 = "ExampleOfPrivateKey1"; + // var walletA = new Wallet(signature, privateKey1); + + // var privateKey2 = "ExampleOfPrivateKey2"; + // var walletB = new Wallet(signature, privateKey2); + + // var wallet = new Wallet( + // new List() + // { + // walletA.Parts.FirstOrDefault(), + // walletB.Parts.FirstOrDefault() + // }); + + // Assert.IsNotNull(wallet.PrivateKey); + // Assert.IsNotEmpty(wallet.PrivateKey); + // //Assert.That(wallet.PrivateKey, Is.Not.EqualTo(privateKey1)); + // //Assert.That(wallet.PrivateKey, Is.Not.EqualTo(privateKey2)); + //} + + #endregion + } +} diff --git a/CoolWallet.Tests/packages.config b/CoolWallet.Tests/packages.config new file mode 100644 index 0000000..d470cb2 --- /dev/null +++ b/CoolWallet.Tests/packages.config @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file From 8c0e3043f450b63e11b8c292149094c71b320eb7 Mon Sep 17 00:00:00 2001 From: fluorine Date: Fri, 25 Aug 2017 23:21:51 -0400 Subject: [PATCH 4/5] Cool Wallet Tool --- CoolWallet.Tool/App.config | 6 + CoolWallet.Tool/CoolWallet.Tool.csproj | 59 ++++++++++ CoolWallet.Tool/Program.cs | 121 +++++++++++++++++++++ CoolWallet.Tool/Properties/AssemblyInfo.cs | 39 +++++++ 4 files changed, 225 insertions(+) create mode 100644 CoolWallet.Tool/App.config create mode 100644 CoolWallet.Tool/CoolWallet.Tool.csproj create mode 100644 CoolWallet.Tool/Program.cs create mode 100644 CoolWallet.Tool/Properties/AssemblyInfo.cs diff --git a/CoolWallet.Tool/App.config b/CoolWallet.Tool/App.config new file mode 100644 index 0000000..bae5d6d --- /dev/null +++ b/CoolWallet.Tool/App.config @@ -0,0 +1,6 @@ + + + + + + diff --git a/CoolWallet.Tool/CoolWallet.Tool.csproj b/CoolWallet.Tool/CoolWallet.Tool.csproj new file mode 100644 index 0000000..ea36754 --- /dev/null +++ b/CoolWallet.Tool/CoolWallet.Tool.csproj @@ -0,0 +1,59 @@ + + + + + Debug + AnyCPU + {BB16AC0C-C3C4-48FF-91F8-AA1E55128856} + Exe + CoolWallet.Tool + CoolWallet.Tool + v4.6.1 + 512 + true + + + + AnyCPU + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + AnyCPU + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + + + + + + + + + + + + + + + + {3326e219-8092-4949-9c3b-dbe619f606d3} + CoolWallet.Core + + + + \ No newline at end of file diff --git a/CoolWallet.Tool/Program.cs b/CoolWallet.Tool/Program.cs new file mode 100644 index 0000000..69e4fe2 --- /dev/null +++ b/CoolWallet.Tool/Program.cs @@ -0,0 +1,121 @@ +using CoolWallet.Core; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace CoolWallet.Tool +{ + class Program + { + const string Version = "v0.0.1-alpha"; + + static void Main(string[] args) + { + try + { + if (args.Length < 1) return; + + switch (args[0].Trim()) + { + case "-g": // Generate wallet + GenerateSharedWallet(args[1].Trim(), args[2].Trim(), args[3].Trim()); + break; + case "-h": // Display help + PrintUsage(); + break; + case "-r": // Recover wallet using parts + if (args.Length > 1) + { + // Process parts provided as arguments + var rawParts = args.Reverse().Take(args.Length - 1); + RecoverPrivateKey(rawParts); + } + else + { + // Get parts by prompting + } + break; + default: + break; + } + } catch(ArgumentException e) + { + Console.WriteLine("Error:"); + Console.WriteLine(" " + e.Message); + } + } + + static void GenerateSharedWallet(string thresholdStr, string totalStr, string privateKey) + { + // Validate and convert values + var valid = !string.IsNullOrWhiteSpace(thresholdStr) + && !string.IsNullOrWhiteSpace(totalStr) + && !string.IsNullOrWhiteSpace(privateKey); + + int threshold = -1; + valid = valid && int.TryParse(thresholdStr, out threshold); + + int total = -1; + valid = valid && int.TryParse(totalStr, out total); + + var signature = new WalletSignature() + { + PartsThreshold = threshold, + PartsTotal = total + }; + + if (!valid || !signature.IsValid()) + { + // Show instructions if parametes are invalid. + PrintUsage(); + } + else + { + // Generate wallet + var wallet = new Wallet(signature, privateKey); + + if(!wallet.IsValid()) + { + PrintUsage(); + return; + } + + // Print parts + Console.WriteLine("Parts generated: "); + foreach(var part in wallet.Parts) + { + Console.WriteLine(" - " + part.GetShortNotation()); + } + } + + } + + static void PrintUsage() + { + Console.WriteLine($"Cool Wallet generator {Version}."); + Console.WriteLine(" - Generate a cool wallet's parts:"); + Console.WriteLine(" -g \"\""); + Console.WriteLine(" - Recover a Private Key from parts:"); + Console.WriteLine(" -r \"\" \"\" [...]"); + Console.WriteLine(" - Show this help:"); + Console.WriteLine(" -h"); + } + + static void RecoverPrivateKey(IEnumerable rawParts) + { + var deserializedParts = new List(); + + foreach(var rawPart in rawParts) + { + var part = new WalletPart(rawPart); + deserializedParts.Add(part); + } + + var wallet = new Wallet(deserializedParts); + + Console.WriteLine("Recovered Private Key:\n " + wallet.PrivateKey); + } + } +} diff --git a/CoolWallet.Tool/Properties/AssemblyInfo.cs b/CoolWallet.Tool/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..41d6a65 --- /dev/null +++ b/CoolWallet.Tool/Properties/AssemblyInfo.cs @@ -0,0 +1,39 @@ +using System.Resources; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("SharedColdWallet.Tool")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("SharedColdWallet.Tool")] +[assembly: AssemblyCopyright("Copyright © 2017")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("bb16ac0c-c3c4-48ff-91f8-aa1e55128856")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("0.0.1.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] +[assembly: NeutralResourcesLanguage("en")] + From 67b615b378a5354a1b8128c1273967c5852768a9 Mon Sep 17 00:00:00 2001 From: fluorine Date: Fri, 25 Aug 2017 23:22:25 -0400 Subject: [PATCH 5/5] Solution --- CoolWallet.sln | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 CoolWallet.sln diff --git a/CoolWallet.sln b/CoolWallet.sln new file mode 100644 index 0000000..41f11fd --- /dev/null +++ b/CoolWallet.sln @@ -0,0 +1,34 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 15 +VisualStudioVersion = 15.0.26403.7 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CoolWallet.Tool", "CoolWallet.Tool\CoolWallet.Tool.csproj", "{BB16AC0C-C3C4-48FF-91F8-AA1E55128856}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CoolWallet.Core", "CoolWallet.Core\CoolWallet.Core.csproj", "{3326E219-8092-4949-9C3B-DBE619F606D3}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CoolWallet.Tests", "CoolWallet.Tests\CoolWallet.Tests.csproj", "{8181DF65-9485-42A8-BF3A-D135192F49EF}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {BB16AC0C-C3C4-48FF-91F8-AA1E55128856}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BB16AC0C-C3C4-48FF-91F8-AA1E55128856}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BB16AC0C-C3C4-48FF-91F8-AA1E55128856}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BB16AC0C-C3C4-48FF-91F8-AA1E55128856}.Release|Any CPU.Build.0 = Release|Any CPU + {3326E219-8092-4949-9C3B-DBE619F606D3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3326E219-8092-4949-9C3B-DBE619F606D3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3326E219-8092-4949-9C3B-DBE619F606D3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3326E219-8092-4949-9C3B-DBE619F606D3}.Release|Any CPU.Build.0 = Release|Any CPU + {8181DF65-9485-42A8-BF3A-D135192F49EF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8181DF65-9485-42A8-BF3A-D135192F49EF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8181DF65-9485-42A8-BF3A-D135192F49EF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8181DF65-9485-42A8-BF3A-D135192F49EF}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal