-
Notifications
You must be signed in to change notification settings - Fork 3
/
StorageProvider.cs
executable file
·217 lines (189 loc) · 8.8 KB
/
StorageProvider.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
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
using System.Text;
using System.Text.Json;
using KVPSButter;
namespace OAuthServer;
/// <summary>
/// Wrapper class that provides encrypted key storage
/// </summary>
public class StorageProvider
{
[Serializable]
public class DecryptingFailedException : Exception
{
public DecryptingFailedException() { }
public DecryptingFailedException(string message) : base(message) { }
public DecryptingFailedException(string message, Exception inner) : base(message, inner) { }
}
/// <summary>
/// Reduce on-disk storage for encrypted entities
/// </summary>
static StorageProvider()
{
SharpAESCrypt.SharpAESCrypt.Extension_InsertPlaceholder = false;
}
/// <summary>
/// The storage interface to use
/// </summary>
private readonly IKVPS m_storage;
/// <summary>
/// The minimum expiration for an access token (when nothing is reported from service)
/// </summary>
public static long MinimumExpirationLength = 1000;
/// <summary>
/// Returns the best-guess expiration time in seconds
/// </summary>
/// <param name="a">One expiration time</param>
/// <param name="b">Another expiration time</param>
/// <returns>The best-guess expiration time</returns>
public static long ExpirationSeconds(long a, long b)
=> Math.Max(Math.Max(a, b), MinimumExpirationLength);
/// <summary>
/// Creates a new storage provider
/// </summary>
/// <param name="storageString">The storage destination</param>
public StorageProvider(string storageString)
{
// If we just get a plain path, map it to the file provider.
// Since the filenames are guaranteed to be alphanumeric we can store them without encoding
if (storageString.IndexOf("://") < 0)
storageString = $"file://{storageString}?pathmapped=true";
m_storage = KVPSLoader.Default.Create(storageString);
}
/// <summary>
/// A parsed remote storage token
/// </summary>
/// <param name="ServiceId">The service the token was created for</param>
/// <param name="Expires">The time when the access token expires</param>
/// <param name="AccessToken">The access token</param>
/// <param name="RefreshToken">The refresh token</param>
/// <param name="Json">The complete original JSON response</param>
public record StoredEntry(string ServiceId, DateTime Expires, string AccessToken, string RefreshToken, string Json);
/// <summary>
/// Internal record for de-serializing the OAuth JSON response
/// </summary>
/// <param name="access_token">The current access token</param>
/// <param name="refresh_token">The refresh token</param>
/// <param name="expires">The number of seconds before the access token expires</param>
private record OAuthEntry(string access_token, string refresh_token, long expires, long expires_in);
/// <summary>
/// Creates a new auth-token
/// </summary>
/// <param name="serviceId">The service to create it for</param>
/// <param name="json">The JSON to store</param>
/// <param name="cancellationToken">The cancellation token to use</param>
/// <returns>The auth-token</returns>
public async Task<string> CreateAuthTokenAsync(string serviceId, string json, CancellationToken cancellationToken)
{
// Generate key and password
string keyId = Guid.NewGuid().ToString("N");
string password = PasswordGenerator.Generate();
var resp = JsonSerializer.Deserialize<OAuthEntry>(json)
?? throw new InvalidDataException("Response JSON could not be deserialized");
var expires = DateTime.UtcNow.AddSeconds(ExpirationSeconds(resp.expires, resp.expires_in));
var entry = new StoredEntry(serviceId, expires, resp.access_token, resp.refresh_token, json);
await EncryptAndWriteEntryAsync(keyId, password, entry, cancellationToken);
return $"{keyId}:{password}";
}
/// <summary>
/// Updates a stored entry
/// </summary>
/// <param name="keyId">The key to update</param>
/// <param name="password">The password to use</param>
/// <param name="json">The json response</param>
/// <param name="cancellationToken">The cancellation token</param>
/// <returns>An awaitable task</returns>
public async Task UpdateEntryAsync(string keyId, string password, string json, CancellationToken cancellationToken)
{
var resp = JsonSerializer.Deserialize<OAuthEntry>(json)
?? throw new InvalidDataException("Response JSON could not be deserialized");
var existing = await GetFromKeyIdAsync(keyId, password, cancellationToken);
var expires = DateTime.UtcNow.AddSeconds(ExpirationSeconds(resp.expires, resp.expires_in));
var updated = existing with
{
Json = json,
Expires = expires,
AccessToken = string.IsNullOrWhiteSpace(resp.access_token)
? existing.AccessToken
: resp.access_token,
RefreshToken = string.IsNullOrWhiteSpace(resp.refresh_token)
? existing.RefreshToken
: resp.refresh_token
};
await EncryptAndWriteEntryAsync(keyId, password, updated, cancellationToken);
}
/// <summary>
/// Retrieves a previously stored token
/// </summary>
/// <param name="keyId">The key ID</param>
/// <param name="password">The password used to decrypt it</param>
/// <param name="cancellationToken">The cancellation token to use</param>
/// <returns>The decrypted instance</returns>
public async Task<StoredEntry> GetFromKeyIdAsync(string keyId, string password, CancellationToken cancellationToken)
{
try
{
using var decrypted = new MemoryStream();
using (var encrypted = await ReadEntryAsync(keyId, cancellationToken))
SharpAESCrypt.SharpAESCrypt.Decrypt(password, encrypted, decrypted);
decrypted.Position = 0;
return JsonSerializer.Deserialize<StoredEntry>(decrypted)
?? throw new InvalidDataException("Failed to parse contents of decrypted file");
}
catch (Exception ex)
{
throw new DecryptingFailedException("Decryption failed, invalid key?", ex);
}
}
/// <summary>
/// Delete a previously stored token
/// </summary>
/// <param name="keyId">The key ID</param>
/// <param name="cancellationToken">The cancellation token to use</param>
/// <returns>An awaitable task</returns>
public Task DeleteByKeyIdAsync(string keyId, CancellationToken cancellationToken)
=> DeleteEntryAsync(keyId, cancellationToken);
/// <summary>
/// Serializes the entry, encrypts it, and writes it to storage
/// </summary>
/// <param name="keyId">The key ID</param>
/// <param name="password">The password to encrypt with</param>
/// <param name="entry">The entry to encrypt</param>
/// <param name="cancellationToken">The cancellation token to use</param>
/// <returns>An awaitable task</returns>
private async Task EncryptAndWriteEntryAsync(string keyId, string password, StoredEntry entry, CancellationToken cancellationToken)
{
var plaintext = Encoding.UTF8.GetBytes(JsonSerializer.Serialize(entry));
using (var enc = new MemoryStream())
{
using (var ms = new MemoryStream(plaintext))
SharpAESCrypt.SharpAESCrypt.Encrypt(password, ms, enc);
enc.Position = 0;
await WriteEntryAsync(keyId, enc, cancellationToken);
}
}
/// <summary>
/// Writes an encrypted entry to persistent storage
/// </summary>
/// <param name="key">The key to use</param>
/// <param name="data">The data to write</param>
/// <param name="cancellationToken">The cancellation token</param>
/// <returns>An awaitable task</returns>
private Task WriteEntryAsync(string key, Stream data, CancellationToken cancellationToken)
=> m_storage.WriteAsync(key, data, cancellationToken);
/// <summary>
/// Reads an encrypted entry from persistent storage
/// </summary>
/// <param name="key">The key to read</param>
/// <param name="cancellationToken">The cancellation token</param>
/// <returns>An awaitable task</returns>
private async Task<Stream> ReadEntryAsync(string key, CancellationToken cancellationToken)
=> await m_storage.ReadAsync(key, cancellationToken) ?? throw new KeyNotFoundException();
/// <summary>
/// Deletes an entry from persistent storage
/// </summary>
/// <param name="key">The key to delete</param>
/// <param name="cancellationToken">The cancellation token</param>
/// <returns>An awaitable task</returns>
private Task DeleteEntryAsync(string key, CancellationToken cancellationToken)
=> m_storage.DeleteAsync(key, cancellationToken);
}