-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathPasswords.cs
413 lines (356 loc) · 15.2 KB
/
Passwords.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
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
/*/ role miniProgram; define SCRIPT; /*/
using System.Security.Cryptography;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Media;
using System.Windows.Input;
#if SCRIPT
//Passwords.Save("saved", "test");
//Passwords.Delete("Saved");
//print.it(Passwords.Get("saved"));
Passwords.ShowManagerUI();
#endif
/// <summary>
/// Simple password manager for scripts.
/// </summary>
/// <remarks>
/// In scripts, you often need to use passwords, API keys, and other kinds of credentials. This class helps to protect them. In script you use password names, not passwords directly; passwords are saved encrypted.
/// To encrypt passwords uses a user-provided encryption key (aka master password). Saves the key encrypted using Windows data protection API, which makes it undecryptable on other computers and user accounts.
/// Saves everything in <see cref="Folder"/>.
/// Can be used in portable apps too; more info in <see cref="Folder"/> remarks.
/// <para>
/// Obviously, passwords managed by this class are not completely secure. And less secure than when using a password manager software.
/// To use in scripts conveniently, this class saves encrypted passwords (and key) so you don't have to enter them manually each time. They can be decrypted only on the same computer/account.
/// A determined hacker can steal the passwords if he can access that computer/account and somehow know about this class and find the saved data and this code (or just run this code). It's unlikely, but possible.
/// Also this class does not encrypt passwords in memory, and does not clear the password string memory.
/// But this class gives full protection from leaking passwords through script sharing or github etc.
/// Another benefit: when you change a password, will not need to find and update it in each script that uses the password. Instead you can run this script to show the password manager UI and update the password there.
/// </para>
/// <para>
/// To avoid password/key input dialogs at an inconvenient time, call any <b>Passwords</b> method at a convenient time. Or run this file (it calls <b>Passwords.ShowManagerUI</b>).
/// </para>
/// </remarks>
public static class Passwords {
#region settings. You can edit this code.
/// <summary>
/// Full path of the password manager data folder.
/// Default: <c>folders.ThisAppDataLocal + "Pm"</c>.
/// </summary>
/// <remarks>
/// In portable LA the folder is in its drive. The portable setup tool does not copy/update it, unless the path is changed like <c>folders.ThisAppDataRoaming + "Pm"</c> (the tool copies the roaming folder).
/// Note: the <b>folders.ThisAppDataLocal/Roaming</b> path is different if script role is <b>editorExtension</b>.
/// </remarks>
public static string Folder { get; set; } = folders.ThisAppDataLocal + "Pm";
/// <summary>
/// Parameter <i>optionalEntropy</i> of <see cref="ProtectedData"/> class functions.
/// </summary>
/// <remarks>
/// Used when encrypting and decrypting the password encryption key. The key cannot be decrypted with different entropy than when encrypting it.
/// Recommended: replace the default value with a unique value of any length. It adds some more security and separates your passwords from passwords of other apps that use this class and same folder.
/// The names of password data and key files depend on this value. It allows multiple apps (or scripts) to save their passwords separately. Just change the default value used by your app.
/// </remarks>
static byte[] _entropy = [39, 212, 196, 74];
/// <summary>
/// Edit this if want to customize the password input dialog. For example localize the text.
/// </summary>
/// <param name="name"></param>
/// <param name="password"></param>
/// <param name="save">true to save the password.</param>
/// <returns><c>true</c> if OK.</returns>
static bool _PasswordInputDialog(string name, out string password, out bool save) {
DControls c = new() { Checkbox = "Save", IsChecked = true };
bool ok = dialog.showInput(out password, null, "Password for " + name, DEdit.Password, controls: c);
save = c.IsChecked;
return ok;
}
/// <summary>
/// Edit this if want to customize the key input dialog. For example localize the text.
/// </summary>
/// <param name="key"></param>
/// <param name="newKey">If <c>true</c>, the user can enter a new key (there are no saved passwords). If <c>false</c>, the user must enter the same key.</param>
/// <returns><c>true</c> if OK.</returns>
static bool _KeyInputDialog(out string key, bool newKey) {
string info = newKey
? "Please enter a key (any text) that will be used to encrypt saved passwords.\nIt will be saved encrypted for this computer/user (undecryptable elsewhere)."
: "Please enter the same key that was used to encrypt the saved passwords.";
return dialog.showInput(out key, "Password manager key", info, DEdit.Password);
}
/// <summary>
/// Edit this if want to customize the "Delete all saved passwords?" dialog. For example localize the text.
/// Called when <b>_KeyInputDialog</b> returns <c>false</c> when <i>newKey</i> <c>false</c>.
/// </summary>
/// <returns><c>true</c> to delete.</returns>
static bool _DeletePasswordsDialog() {
return 1 == dialog.show("Delete all saved passwords?",
"Saved passwords cannot be decrypted without the key.\nIf you have lost the key, you can delete saved passwords and set a new key.\nDo you want to delete all saved passwords?",
"1 Delete|2 Cancel", 0, DIcon.Warning, defaultButton: 2);
}
#endregion
static string _PasswordsFile => Folder + "\\" + (_filename ??= Hash.MD5(_entropy).ToString()) + ".csv";
static string _filename;
static string _KeyFile => _PasswordsFile + ".key";
static csvTable _LoadCsv(string file = null) {
file ??= _PasswordsFile;
if (!filesystem.exists(file).File) return new() { ColumnCount = 2 };
var t = csvTable.load(file);
if (t.ColumnCount < 2) t.ColumnCount = 2;
return t;
}
static Dictionary<string, string> _LoadDict() => _LoadCsv().ToDictionary(true, true);
static void _SaveCsv(csvTable t) {
t.Save(_PasswordsFile);
}
static void _SaveDict(Dictionary<string, string> d) => _SaveCsv(csvTable.fromDictionary(d));
static byte[] _GetKey() {
try {
var data = filesystem.loadBytes(_KeyFile);
var r1 = ProtectedData.Unprotect(data, _entropy, DataProtectionScope.CurrentUser);
if (r1.Length == 16) return r1;
}
catch { }
return _SetKey();
}
static bool _KeyInputDialog2(out byte[] key, bool newKey) {
key = null;
string s = "";
while (s.Length < 1) {
if (!_KeyInputDialog(out s, newKey)) return false;
}
key = Hash.MD5(s).ToArray();
return true;
}
static byte[] _SetKey() {
var t = _LoadCsv();
bool newKey = t.RowCount == 0;
g1:
if (!_KeyInputDialog2(out var key, newKey)) { //canceled
if (newKey || !_DeletePasswordsDialog()) throw new OperationCanceledException();
filesystem.delete(_PasswordsFile);
filesystem.delete(_KeyFile);
newKey = true;
goto g1;
}
//is data encrypted with this key?
if (!newKey && t.Rows.All(a => !_Decrypt(a[1], key, out _))) { //if fails to decrypt all passwords, the key is incorrect
500.ms();
goto g1;
}
var p = ProtectedData.Protect(key, _entropy, DataProtectionScope.CurrentUser);
filesystem.saveBytes(_KeyFile, p);
return key;
}
static bool _ChangeKey(ref byte[] key, List<_Item> a) {
if (!_KeyInputDialog2(out var key2, newKey: true)) return false;
foreach (var v in a) {
if (_Decrypt(v.EncryptedPassword, key, out string pw))
v.SetNewPassword(pw, key2);
}
var p = ProtectedData.Protect(key2, _entropy, DataProtectionScope.CurrentUser);
filesystem.saveBytes(_KeyFile, p);
key = key2;
return true;
}
static string _Encrypt(string s, byte[] key = null) {
if (s.NE()) return "";
return Convert2.AesEncryptS(s, key ?? _GetKey());
}
static bool _Decrypt(string s, byte[] key, out string r) {
if (s.NE()) r = "";
else {
try { r = Convert2.AesDecryptS(s, key); }
catch { r = null; return false; }
}
return true;
}
//TODO: in name, replace substring <user> with user SID.
/// <summary>
/// Encrypts <i>password</i> and saves in the passwords file. Can add or replace.
/// </summary>
/// <param name="name">A name for the password. Case-insensitive.</param>
/// <param name="password"></param>
/// <exception cref="OperationCanceledException">Key input dialog canceled.</exception>
/// <exception cref="Exception">Failed to load (if exists) or save the passwords file.</exception>
public static void Save(string name, string password) {
_Save(_LoadDict(), name, password);
}
static void _Save(Dictionary<string, string> d, string name, string password) {
d[name] = _Encrypt(password);
_SaveDict(d);
}
/// <summary>
/// Deletes one or more passwords from the passwords file.
/// </summary>
/// <param name="names">Password names. Case-insensitive.</param>
/// <exception cref="Exception">Failed to load (if exists) or save the passwords file.</exception>
public static void Delete(params string[] names) {
var d = _LoadDict();
bool deleted = false;
foreach (var v in names) deleted |= d.Remove(v);
if (deleted) _SaveDict(d);
}
/// <summary>
/// Gets a password from the passwords file and decrypts.
/// If not found, shows a password input dialog and calls <see cref="Save"/> (optionally).
/// </summary>
/// <param name="name">The password's name. Case-insensitive.</param>
/// <returns>Password.</returns>
/// <exception cref="OperationCanceledException">Password input dialog canceled. Or key input dialog canceled.</exception>
/// <exception cref="Exception">Failed to load (if exists) or save the passwords file.</exception>
public static string Get(string name) {
var d = _LoadDict();
if (d.TryGetValue(name, out var s)) {
var key = _GetKey();
if (_Decrypt(s, key, out var r)) return r;
}
if (!_PasswordInputDialog(name, out s, out bool save)) throw new OperationCanceledException();
if (save) _Save(d, name, s);
return s;
}
/// <summary>
/// Gets all names.
/// </summary>
/// <exception cref="Exception">Failed to load (if exists) the passwords file.</exception>
/// <example>
/// <code><![CDATA[
/// //show menu with all password names
/// var a = Passwords.GetList();
/// if (a.Any()) {
/// var m = new popupMenu("0cfd5f9c-8a23-4534-ad93-5af3ba8c2b41");
/// foreach (var v in a) {
/// m[v] = o => { print.it(Passwords.Get(v)); };
/// }
/// m.Show();
/// }
/// ]]></code>
/// </example>
public static string[] GetList() {
var d = _LoadDict();
return d.Keys.ToArray();
}
/// <summary>
/// Shows a dialog window with a data grid where you can add, delete and edit names and passwords.
/// </summary>
/// <exception cref="OperationCanceledException">Key input dialog canceled.</exception>
/// <exception cref="Exception">Failed to load (if exists) or save the passwords file.</exception>
public static void ShowManagerUI(Window owner = null) {
var b = new wpfBuilder("Passwords").WinSize(500, 500);
var w = b.Window;
b.R.Add(out Menu menu).Margin("T-2 B0");
var g = new DataGrid {
AutoGenerateColumns = false,
CanUserAddRows = true,
VerticalGridLinesBrush = Brushes.LightGray,
HorizontalGridLinesBrush = Brushes.LightGray,
};
b.Row(-1).Add(g);
b.R.AddOkCancel();
b.End();
var colName = new DataGridTextColumn {
Header = "Name",
Binding = new Binding("Name"),
Width = new(1, DataGridLengthUnitType.Star),
CanUserReorder = false
};
g.Columns.Add(colName);
var colPw = new DataGridTextColumn {
Header = "Password",
Binding = new Binding("Password"),
Width = new(1, DataGridLengthUnitType.Star),
CanUserReorder = false
};
g.Columns.Add(colPw);
var key = _GetKey(); //OperationCanceledException
var t = _LoadCsv();
var a = t.Rows.Select(o => new _Item(o[0], o[1], key)).ToList();
g.ItemsSource = a;
//clear the displayed password placeholder text when started editing
g.PreparingCellForEdit += (o, e) => {
if (e.Column == colPw) {
var tb = (TextBox)e.EditingElement;
if (e.EditingEventArgs is not TextCompositionEventArgs) tb.Clear();
//tb.Foreground = tb.Background; //hide password
}
};
//validate name when ending editing
g.CellEditEnding += (o, e) => {
if (e.EditAction == DataGridEditAction.Commit) {
var tb = (TextBox)e.EditingElement;
var s = tb.Text;
if (s.Trim() is var s2 && s2 != s) {
switch (dialog.show(null, "The texts starts or ends with spaces.", "1 Trim spaces|2 Don't trim|3 Cancel", owner: w, defaultButton: 3)) {
case 3: _Cancel(); return;
case 1: tb.Text = s = s2; break;
}
}
if (e.Column == colName) {
var item = e.Row.Item as _Item;
if (a.Any(v => v != item && v.Name.Eqi(s))) {
dialog.show("Error", $"Name '{s}' already exists.", owner: w);
_Cancel();
}
}
void _Cancel() {
e.Cancel = true;
//workaround for DataGrid bug: on Tab key starts editing next cell. Then 2 cells are in edit mode.
EventHandler<DataGridBeginningEditEventArgs> eh1 = (o, e) => { e.Cancel = true; tb.Focus(); };
g.BeginningEdit += eh1;
timer.after(1, _ => { g.BeginningEdit -= eh1; });
}
}
};
_CreateMenu();
if (owner != null) {
w.Owner = owner;
w.ShowInTaskbar = false;
}
if (!b.ShowDialog(owner)) return;
_SaveItems();
void _SaveItems() {
var t = new csvTable { ColumnCount = 2 };
foreach (var v in a) t.AddRow(v.Name, v.EncryptedPassword);
_SaveCsv(t);
}
void _CreateMenu() {
//File
var mFile = _TopItem("_Menu");
_Item(mFile, "Change key...", o => {
g.CancelEdit();
if (a.Any(o => o.Password == "<error>")) { //unlikely
dialog.showError("Cannot change the key", "Failed to decrypt some passwords (<error>), therefore cannot encrypt them with a new key.\nEdit or delete the <error> passwords.");
return;
}
if (_ChangeKey(ref key, a))
_SaveItems();
});
//TODO2: UI help.
MenuItem _Item(ItemsControl parent, string name, Action<MenuItem> click = null, string tooltip = null) {
var mi = new MenuItem { Header = name, ToolTip = tooltip };
if (click != null) mi.Click += (sender, _) => click(sender as MenuItem);
parent.Items.Add(mi);
return mi;
}
MenuItem _TopItem(string name) => _Item(menu, name);
//void _Separator(ItemsControl parent) { parent.Items.Add(new Separator()); }
}
}
record class _Item {
string _pwDisplay;
public _Item(string name, string encPw, byte[] key) {
Name = name;
EncryptedPassword = encPw;
if (!encPw.NE()) _pwDisplay = _Decrypt(EncryptedPassword, key, out _) ? "•…" : "<error>";
}
public _Item() { }
public string Name { get; set; }
public string EncryptedPassword;
public string Password {
get => _pwDisplay;
set => SetNewPassword(value);
}
public void SetNewPassword(string s, byte[] key = null) {
EncryptedPassword = _Encrypt(s, key);
_pwDisplay = s.NE() ? null : "•…";
}
}
}