-
Notifications
You must be signed in to change notification settings - Fork 141
/
Web3KeyStore.cs
190 lines (170 loc) · 6.49 KB
/
Web3KeyStore.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using Serilog;
namespace Libplanet.KeyStore
{
/// <summary>
/// <a href="https://github.com/ethereum/wiki/wiki/Web3-Secret-Storage-Definition">Web3 Secret
/// Storage</a> (i.e., Ethereum-style key store) compliant <see cref="IKeyStore"/>
/// implementation. Key files are placed in a directory of the <see cref="Path"/>.
/// <para>Use <see cref="DefaultKeyStore"/> property to get an instance.</para>
/// <para>In order to get an instance with a customized directory,
/// use the <see cref="Web3KeyStore(string)"/> constructor.</para>
/// </summary>
public class Web3KeyStore : IKeyStore
{
private static readonly string DefaultPath = System.IO.Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) is { } p && p.Any()
? p
: System.IO.Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
".config"),
"planetarium",
"keystore"
);
private static readonly string NameFormat =
"UTC--{0:yyyy-MM-dd}T{0:HH-mm-ss}Z--{1:D}";
private static readonly Regex NamePattern = new Regex(
@"^UTC--\d{4}-\d\d-\d\dT\d\d-\d\d-\d\dZ--([\da-f]{8}-?(?:[\da-f]{4}-?){3}[\da-f]{12})$",
RegexOptions.Compiled | RegexOptions.CultureInvariant | RegexOptions.IgnoreCase
);
private readonly ILogger _logger;
/// <summary>
/// Creates a <see cref="Web3KeyStore"/> instance with a custom directory
/// <paramref name="path"/>.
/// </summary>
/// <param name="path">A path of the directory to store key files. A new directory is
/// created if not exists.</param>
/// <exception cref="ArgumentNullException">Thrown when <c>null</c> is passed to
/// <paramref name="path"/>.</exception>
/// <seealso cref="DefaultKeyStore"/>
public Web3KeyStore(string path)
{
_logger = Log.ForContext<Web3KeyStore>().ForContext("DirectoryPath", path);
if (path is null)
{
throw new ArgumentNullException(nameof(path));
}
else if (!Directory.Exists(path))
{
Directory.CreateDirectory(path);
_logger.Debug(
"Created a directory {DirectoryPath} as it did not exist.",
path
);
}
Path = path;
}
/// <summary>
/// A default <see cref="Web3KeyStore"/> instance which refers to a user-local directory.
/// The <see cref="Path"/> differs on the platform:
/// <list type="table">
/// <listheader>
/// <term>OS</term>
/// <description>Directory path</description>
/// </listheader>
/// <item>
/// <term>Linux/macOS</term>
/// <description><var>$HOME</var>/.config/planetarium/keystore</description>
/// </item>
/// <item>
/// <term>Windows</term>
/// <description><var>%AppData%</var>\planetarium\keystore</description>
/// </item>
/// </list>
/// </summary>
/// <seealso cref="Web3KeyStore(string)"/>
public static Web3KeyStore DefaultKeyStore =>
new Web3KeyStore(DefaultPath);
/// <summary>
/// The path of the directory key files are placed.
/// </summary>
public string Path { get; }
/// <inheritdoc/>
public IEnumerable<Tuple<Guid, ProtectedPrivateKey>> List() =>
ListFiles().Select(pair => Tuple.Create(pair.Item1, Get(pair.Item2)));
/// <inheritdoc/>
public IEnumerable<Guid> ListIds() =>
ListFiles().Select(pair => pair.Item1);
/// <inheritdoc/>
public ProtectedPrivateKey Get(Guid id)
{
IEnumerable<(Guid, string)> files = ListFiles();
string name;
try
{
(_, name) = files.First(pair => pair.Item1.Equals(id));
}
catch (InvalidOperationException)
{
throw new NoKeyException("There is no key with such ID", id);
}
return Get(name);
}
/// <inheritdoc/>
public Guid Add(ProtectedPrivateKey key)
{
if (key is null)
{
throw new ArgumentNullException(nameof(key));
}
Guid keyId = Guid.NewGuid();
string filename = string.Format(
CultureInfo.InvariantCulture,
NameFormat,
DateTimeOffset.UtcNow,
keyId
);
var keyPath = System.IO.Path.Combine(Path, filename);
using Stream f = new FileStream(keyPath, FileMode.CreateNew);
key.WriteJson(f, keyId);
return keyId;
}
/// <inheritdoc/>
public void Remove(Guid id)
{
foreach ((Guid keyId, string keyPath) in ListFiles())
{
if (keyId.Equals(id))
{
System.IO.File.Delete(keyPath);
return;
}
}
throw new NoKeyException("No key have such ID", id);
}
private IEnumerable<(Guid, string)> ListFiles()
{
IEnumerable<string> keyPaths = Directory.EnumerateFiles(Path);
foreach (string keyPath in keyPaths)
{
if (System.IO.Path.GetFileName(keyPath) is string f)
{
Match m = NamePattern.Match(f);
if (m.Success)
{
if (!Guid.TryParse(m.Groups[1].Value, out Guid id))
{
_logger.Debug(
"Failed to parse the file name due to invalid UUID: {keyPath}"
);
continue;
}
yield return (id, keyPath);
}
}
}
}
private ProtectedPrivateKey Get(string name)
{
using (StreamReader reader = new StreamReader(System.IO.Path.Combine(Path, name)))
{
return ProtectedPrivateKey.FromJson(reader.ReadToEnd());
}
}
}
}