Skip to content

Commit 798df5a

Browse files
committed
Added encryption functionality and documentation for the app
1 parent 3813f2c commit 798df5a

File tree

4 files changed

+202
-6
lines changed

4 files changed

+202
-6
lines changed

NoteD/Program.cs

Lines changed: 77 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using Terminal.Gui;
22
using System.Collections.ObjectModel;
3+
using System.Security.Cryptography;
34
using System.Text.Json;
45
using System.Text.Json.Serialization;
56
using NoteD;
@@ -39,6 +40,38 @@
3940
ThemeManager.Initialize();
4041
ThemeManager.ApplyTheme(SettingsManager.Settings.Theme);
4142

43+
string? masterPassword = null;
44+
45+
var loginPromptDialog = new Dialog { Title = "NoteD Login", Modal = true };
46+
var loginLabel = new Label("Enter your master password. If you forget this password, THERE IS NO WAY TO RESET IT!!") { X = 1, Y = 1 };
47+
var loginInput = new TextField("") { X = 1, Y = 2, Width = Dim.Fill() - 2, Secret = true };
48+
var okBtn = new Button("Login") { IsDefault = true };
49+
var exitBtn = new Button("Exit");
50+
51+
loginPromptDialog.Add(loginLabel, loginInput);
52+
loginPromptDialog.AddButton(okBtn);
53+
loginPromptDialog.AddButton(exitBtn);
54+
55+
okBtn.Clicked += () => {
56+
if (string.IsNullOrWhiteSpace(loginInput.Text.ToString())) {
57+
MessageBox.ErrorQuery("Error", "Password cannot be empty", "OK");
58+
return;
59+
}
60+
masterPassword = loginInput.Text.ToString();
61+
Application.RequestStop();
62+
};
63+
64+
exitBtn.Clicked += () => {
65+
Application.RequestStop();
66+
Environment.Exit(0);
67+
};
68+
69+
Application.Run(loginPromptDialog);
70+
71+
if (string.IsNullOrWhiteSpace(masterPassword)) Environment.Exit(0);
72+
73+
var security = new SecurityModule();
74+
4275
var listView = new ListView
4376
{
4477
X = 0,
@@ -59,8 +92,8 @@
5992
CanFocus = true
6093
};
6194

62-
var cursor = "Line: 1, Col: 1";
63-
var stats = "Chars: 0, Words: 0";
95+
const string cursor = "Line: 1, Col: 1";
96+
const string stats = "Chars: 0, Words: 0";
6497

6598
var statusBar = new StatusBar([
6699
new StatusItem(Key.Null, cursor, null),
@@ -73,6 +106,29 @@
73106
var notesFolder = handler.LoadOrChooseNotesFolder();
74107
context.NotesFolder = notesFolder;
75108

109+
handler.RefreshNoteList();
110+
111+
foreach (var note in context.NoteFiles)
112+
{
113+
try
114+
{
115+
security.UnprotectFile(Path.Combine(notesFolder, note), masterPassword);
116+
}
117+
catch (InvalidDataException)
118+
{
119+
Console.WriteLine($"File {note} is not encrypted. Skipping decryption.");
120+
}
121+
catch (CryptographicException)
122+
{
123+
MessageBox.ErrorQuery("Access Denied", "Incorrect Password.", "OK");
124+
Environment.Exit(0);
125+
}
126+
catch (Exception ex)
127+
{
128+
Console.WriteLine($"Error decrypting file {note}: {ex.Message}");
129+
}
130+
}
131+
76132
textView.KeyPress += (key) =>
77133
{
78134
if (key.KeyEvent.Key >= Key.Space ||
@@ -133,7 +189,14 @@
133189

134190
win.Add(listView, textView);
135191

136-
Application.Top.Add(menu, win, statusBar);
192+
var top = new Toplevel {
193+
X = 0,
194+
Y = 0,
195+
Width = Dim.Fill(),
196+
Height = Dim.Fill()
197+
};
198+
199+
top.Add(menu, win, statusBar);
137200

138201
if (SettingsManager.Settings.AutoSaveEnabled)
139202
{
@@ -177,7 +240,17 @@
177240
handler.UpdateStatusBar();
178241
};
179242

180-
Application.Run();
243+
Application.Run(top);
244+
245+
foreach (var note in noteFiles)
246+
{
247+
var path = Path.Combine(notesFolder, note);
248+
if (File.Exists(path))
249+
{
250+
security.ProtectFile(path, masterPassword);
251+
}
252+
}
253+
181254
autoSaveTimer?.Dispose();
182255
Application.Shutdown();
183256

NoteD/Security.cs

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
using Konscious.Security.Cryptography;
2+
using System.Security.Cryptography;
3+
using System.Text;
4+
5+
namespace NoteD;
6+
7+
public class SecurityModule
8+
{
9+
private static readonly byte[] MagicBytes = { 0x4E, 0x6F, 0x74, 0x65, 0x44 };
10+
11+
private const int DegreeOfParallelism = 4;
12+
private const int Iterations = 4;
13+
private const int MemorySize = 1024 * 128;
14+
15+
// Metadata Lengths (Total Header: 81 bytes)
16+
private const int SaltSize = 16;
17+
private const int NonceSize = 12;
18+
private const int TagSize = 16;
19+
private const int HashSize = 32;
20+
private const int HeaderSize = 5 + SaltSize + NonceSize + TagSize + HashSize;
21+
22+
private (byte[] encryptionKey, byte[] verificationHash) DeriveKeys(string password, byte[] salt)
23+
{
24+
using var argon2 = new Argon2id(Encoding.UTF8.GetBytes(password));
25+
argon2.Salt = salt;
26+
argon2.DegreeOfParallelism = DegreeOfParallelism;
27+
argon2.Iterations = Iterations;
28+
argon2.MemorySize = MemorySize;
29+
30+
var combined = argon2.GetBytes(64);
31+
return (combined[..32], combined[32..]);
32+
}
33+
34+
/// <summary>
35+
/// Reads an encrypted file, decrypts it, and overwrites it with Plaintext.
36+
/// </summary>
37+
public void UnprotectFile(string filePath, string password)
38+
{
39+
var fileData = File.ReadAllBytes(filePath);
40+
41+
if (fileData.Length < HeaderSize || !fileData[..5].SequenceEqual(MagicBytes))
42+
{
43+
throw new InvalidDataException("This is not a valid NoteD encrypted file.");
44+
}
45+
46+
var salt = fileData[5..21];
47+
var nonce = fileData[21..33];
48+
var tag = fileData[33..49];
49+
var expectedHash = fileData[49..81];
50+
var ciphertext = fileData[81..];
51+
52+
var (key, vHash) = DeriveKeys(password, salt);
53+
54+
if (!FixedTimeEquals(vHash, expectedHash))
55+
throw new CryptographicException("Invalid Password.");
56+
57+
using var aes = new AesGcm(key, 16);
58+
var plaintextBytes = new byte[ciphertext.Length];
59+
aes.Decrypt(nonce, ciphertext, tag, plaintextBytes);
60+
61+
File.WriteAllBytes(filePath, plaintextBytes);
62+
}
63+
64+
/// <summary>
65+
/// Reads a plaintext file, encrypts it, and overwrites it with Binary Header and Ciphertext.
66+
/// </summary>
67+
public void ProtectFile(string filePath, string password)
68+
{
69+
var content = File.ReadAllText(filePath);
70+
var salt = CreateRandomSalt();
71+
var (key, vHash) = DeriveKeys(password, salt);
72+
73+
using var aes = new AesGcm(key, 16);
74+
var nonce = new byte[NonceSize];
75+
RandomNumberGenerator.Fill(nonce);
76+
var tag = new byte[TagSize];
77+
var plaintextBytes = Encoding.UTF8.GetBytes(content);
78+
var ciphertext = new byte[plaintextBytes.Length];
79+
aes.Encrypt(nonce, plaintextBytes, ciphertext, tag);
80+
81+
using var ms = new MemoryStream();
82+
using var writer = new BinaryWriter(ms);
83+
writer.Write(MagicBytes);
84+
writer.Write(salt); // 16
85+
writer.Write(nonce); // 12
86+
writer.Write(tag); // 16
87+
writer.Write(vHash); // 32
88+
writer.Write(ciphertext); // Rest
89+
90+
File.WriteAllBytes(filePath, ms.ToArray());
91+
}
92+
93+
private byte[] CreateRandomSalt() => RandomNumberGenerator.GetBytes(SaltSize);
94+
95+
private static bool FixedTimeEquals(byte[] left, byte[] right)
96+
{
97+
return CryptographicOperations.FixedTimeEquals(left, right);
98+
}
99+
}

README.md

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,24 @@
11
# NoteD
22

3-
A simple TUI markdown note taker made in C#.
3+
A simple TUI markdown note taker made in C#.
4+
5+
## Features
6+
- Auto-save
7+
- Settings in settings.json
8+
- Fuzzy search
9+
- Note management inside folders
10+
- Command Palette (VSCode-style)
11+
- Color themes (+ support for custom themes)
12+
- #Tag support inside notes for faster searches
13+
- Smart status bar (character & word count, cursor position)
14+
- Recent notes list
15+
- External editor hook
16+
17+
## Setup
18+
1. Build from source or download compiled binary from the latest release.
19+
20+
2. To use the external editor hook, make sure the external editor's binary is in the environment PATH.
21+
22+
3. When setting up a password, make sure to store it properly, if you forget it, it can't be reset, so you'll lose access to all your files. **I do not take responsibility for any important file loss!**
23+
24+
4. On startup of NoteD, you have to give it time to decrypt all of the files. To increase encryption and decryption speed, you need to change the `DegreeOfParallelism` variable inside of Security.cs to a higher number to use more CPU threads. Also when you leave NoteD, it starts encrypting all the files, you have to wait until it finishes, otherwise your files will NOT be protected.

TODO.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
- Features to add:
2-
- Encryption for notes
32
- Syncing to git, Nextcloud (WebDAV)
43
- Advanced formatting options (tables, code blocks, etc)
54
- Task management (to-do lists, reminders)
5+
- Recovery key for lost password (maybe at v2)
6+
- Memory zeroing after exit (maybe at v2)
7+
- Storing decrypted notes & password hash in RAM (maybe at v2)
68
- Note sharing (public/private links) (maybe at v2)
79
- Plugin system for third-party extensions (maybe at v2)
810

@@ -21,3 +23,4 @@
2123
- Smart status bar (Line/Column number, size in words and characters, file saved timestamp, read-only status)
2224
- Recent notes list
2325
- External editor hook
26+
- Encryption for notes (Master password)

0 commit comments

Comments
 (0)