Notepad2e icon indicating copy to clipboard operation
Notepad2e copied to clipboard

Add encryption

Open nhantrn opened this issue 6 years ago • 9 comments

Can you add the encryption module from https://github.com/RaiKoHoff/notepad2-mod-crypto ? It's based on XhmikosR's build, but it's been archived.

nhantrn avatar Dec 12 '18 23:12 nhantrn

Are you really using a text editor with built-in encryption?

Why not use some other program that allows working with encrypted file containers? It's much more reliable and flexible and doesn't involve temporary files (temporary files are the weak spot of archive-based encryption like 7-zip, that's true).

ProgerXP avatar Dec 13 '18 19:12 ProgerXP

I'm using it as secondary protection and for non critical info. Just something quick and dirty for around the office use.

nhantrn avatar Dec 14 '18 03:12 nhantrn

I see what you mean. It's possible we may implement it but much simpler (incompatible with that mod). I see no reason to use any libraries for software encryption and/or hashing, Windows CryptoAPI is more than enough for such use case.

Anyway, we have to close some other tasks first.

ProgerXP avatar Dec 14 '18 16:12 ProgerXP

@cshnik

General notes

  • unlike for other Notepad2 code, C++ can be used in this module
  • encryption format is compatible with OpenSSL's command-line tool when using salted passwords (-pass)
  • all functions operate on the current buffer's contents in-memory, not on the original file (or temp files) - this allows e.g. armouring (base64, etc.), transforming/obfuscating, storing inside other data, etc.
    • assume that the data is many times smaller than the available memory (as other encode/decode functions) so no need for complex buffering
  • for simplicity we're using only standard Windows CryptoAPI available since XP (not CNG)
  • CryptoAPI has "simple message functions" but we'll have to use "low-level" functions because the former have unknown format
  • we are using MD5 for key derivation (1 iteration), AES-256-CBC for data encryption, standard PKCS#5 padding
    • MD5 is the only supported message digest (-md) on older OpenSSL versions (1.0.x)
    • signing is not used
  • password entry happens on secure desktop (see e.g. KeePass)
  • password is stored in-memory until the process exits
  • when intermediate data like IV is not needed it should be wiped using RtlSecureZeroMemory (https://msdn.microsoft.com/en-us/2c4090a6-025b-4b7b-8f31-7e744ad51b39), not ZeroMemory or memset

Encrypted data formats

There are 3 flavours of the same format.

Format 1 is OpenSSL's verbatim format: magic string = Salted__, 8-byte salt, encrypted data.

Format 2 is format 1 without the magic string (so there's no identification and the data looks like random noise).

Format 3 is format 1 prepended with key slot(s). Each slot is a format 2 data - a master passphrase (mpassphrase) encrypted with different user-supplied passphrases. Each slot is 40 bytes long (8 for salt, 32 for AES block size). Data of format 1 (which follows the key slots) is encrypted with mpassphrase.

Number of slots may be different but never more than MaxKeySlots. This number is calculated by dividing offset of Salted__ (format 1 start) by 40.

The main data is encrypted with a common mpassphrase and master salt (msalt). See openssl enc examples in the File command section.

 _______________ format 3 ______________
/                                       \
[slot 1] [slot 2] ... Salted__ msalt data
40 B     40 B         8 B      8 B   *
format 2 format 2     \____ format 1 ___/

Format detection:

  • if data begins with Salted__ - format 1
  • if data contains Salted__ within the first MaxKeySlots * 40 bytes + 1 (5 * 40 + 1 = 201) and Salted__ 0-based offset is even to 40 - format 3
  • else - format 2

Therefore Notepad 2e must have 2 internal fundamental encryption functions: one for format 2 encryption, another - for format 2 decryption, both using CryptoAPI. Other 2 formats' functions are based on these 2 functions.

You can use this openssl enc command to generate encrypted data in format 1 (result is 32-byte long, note that each time it's different because of the random salt):

echo -n hello | openssl aes-256-cbc -md md5 -pass pass:sEcReT | xxd

To decrypt, use the -d switch:

openssl aes-256-cbc -md md5 -pass pass:sEcReT -d -in encrypted.file

With regular encryption, AES needs an IV and a key. openssl enc derives both from the salt and the password - see EVP_BytesToKey() in OpenSSL's sources.

AES-256's key length is 32 bytes, IV length is 16 bytes.

New commands

  • File > Encrypte&d
    • change accelerator of Create &Desktop Link to ...Lin&k
    • supports formats 1, 3
    • this is a checkbox, corresponding to EncryptedFileIO
  • Edit > Encode > &Encrypt
    • supports formats 1, 2
  • Edit > Encode > &Decrypt
    • supports formats 1, 2

New internal variables (proposed)

  • EncryptedFileIO (bool)
    • reset on a new file, file open/reload/etc.
  • CachedKeySlots (char[])
    • reset on a new file, file open/reload/etc.
  • CachedPassphrase (char[])
    • never reset (only when the process exits)
    • I suppose we should RtlSecureZeroMemory it on WM_CLOSE?

New internal constant

  • MaxKeySlots = 5

New dialog

There is a single new dialog - prompt for the password(s). It's shown on secure desktop.

Besides OK/Cancel buttons it contains N inputs (N = MaxKeySlots): Passphrase &N: (e.g. Passphrase &2:).

  • passphrase 1 must be non-empty on submit (disable OK if blank)
  • if CachedPassphrase is non-null, passphrase 1 is pre-filled with that string before showing
  • on submit, CachedPassphrase is set to passphrase 1
  • inputs 2/3/.../MaxKeySlots are only enabled for File > Encrypted command

Encode commands

Both work on the selection or, if it's blank, on the entire buffer. If the operation was successful, the selection (buffer) is replaced by the new content, and re-selected (if there was no selection, entire buffer is replaced and selected). If failed, selection is unchanged and user sees an error message.

Encrypt displays the prompt and produces a data stream (cipher-text) in format 1 (because it's binary, user should enable Binary-Safe Save #170).

Decrypt displays the prompt and produces another data stream (plain-text), or possibly junk if the passphrase specified were incorrect (there's no verification except padding).

File command

Works on the entire buffer ignoring current selection, and resets selection to the start of the buffer when called.

EncryptedFileIO indicates if the buffer should be silently encrypted when saving (FileIO).

When File > Encrypted is called and EncryptedFileIO is true - just set EncryptedFileIO to false. (Reload/F5 would reset EncryptedFileIO and display the garbled encrypted data.)

When File > Encrypted is called and EncryptedFileIO is false - check if the buffer is in format 1 or 3 (not 2).

If it is format 1, display the prompt, decrypt the entire buffer (replace window text), set CachedKeySlots to null and set EncryptedFileIO to true.

  • Undo/Redo history should be cleared if non-empty - display a prompt before decrypting (see how File > Encroding > UTF-8 behaves)

If it is format 3, display the prompt, iterate over all key slots (each in format 2), decrypt a slot (mpassphrase), decrypt the main data (format 1), replace the window's text with decrypted buffer, set CachedKeySlots (part before format 3, Salted__) and set EncryptedFileIO to true.

  • same Undo/Redo considerations as above for format 1

if neither 1 nor 3, display the prompt and set EncryptedFileIO to true. Do not actually encrypt/change the buffer's contents/window text.

  • in this prompt, all Passphrase inputs are enabled
  • if at least 2 inputs were not blank on submit, create that many key slots in memory and store in CachedKeySlots
    • use random (junk) passphrase for blank inputs
    • generate a 20-byte long mpassphrase using a CSPRNG
    • set CachedPassphrase to Passphrase &1
    • example: inputs 1 and 3 are not blank, MaxKeySlots is 5; thus 3 key slots are created: 1st with Passphrase &1, 2nd with junk (unusable), 3rd with Passphrase &3
  • if only Passphrase 1 is not blank, set it to CachedPassphrase and set CachedKeySlots to null

Intended usage scenario: user starts with cipher-text or plain-text, calls File > Encrypt to mark it as "encrypt-on-save" and possibly decrypt it; then continues to edit the same plain-text and when he saves it, it gets encrypted internally thanks to the FileIO change (below).

Note: if Encrypt/Decrypt prompts are cancelled or if a encryption/decryption error occurs, this is treated as command failure and global variables' and window's state is unchanged.

FileIO change

On writing to a file (FileIO), if EncryptedFileIO is true the buffer is silently encrypted as format 1 or 3 and then written (window's text and global variables are left as is).

  • if CachedKeySlots is not null, produces format 3 with CachedKeySlots simply copied to before Salted__ (because mpassphrase remains the same and user's CachedPassphrase is able to decrypt one of the key slots)
  • if CachedKeySlots is null and CachedPassphrase is not null, produces format 1
  • if both are null but EncryptedFileIO is true, this is an error

Notes:

  • naturally, such encrypted buffer should not be affected by Line Endings and other text-related settings, it must be saved as-is regardless of #170
  • there are no special commands to change passphrases of format 3 using this program but it can be done using Encode commands: open an encrypted format 3 file, Decrypt one of the key slots (40 bytes) - result would be mpassphrase (N <= 32 bytes); next Encrypt it with another passphrase (40 bytes again) and Save the file
  • I'm not sure how/if this should work with #166 but possibly it might work even elevated given the low-level FileIO is overloaded. This is not important though, so if it doesn't work then one of the commands (Encrypt, Elevate) must be disabled if another is checked.

openssl enc examples

In the end, when using File > Encrypted, it should be possible to decrypt openssl enc streams as well as produce streams recognized by openssl enc -d.

Example of producing a format 3 data using standard tools:

dd if=/dev/urandom of=mpassphrase bs=20 count=1
echo -n hello | openssl aes-256-cbc -md md5 -pass fd:3 3<mpassphrase -out format-1
openssl aes-256-cbc -md md5 -pass pass:sEcReT -in mpassphrase | xxd -p -seek 8 | xxd -r -p >key-slot-1
openssl aes-256-cbc -md md5 -pass pass:pRiNcEsS -in mpassphrase | xxd -p -seek 8 | xxd -r -p >key-slot-2
cat key-slot-* format-1 >format-3

To decode a format-3 given any one of the key slots' passphrases (no mpassphrase):

xxd -p -seek 0  -len 40 format-3 | xxd -r -p >key-slot-1
xxd -p -seek 40 -len 40 format-3 | xxd -r -p >key-slot-2
xxd -p -seek 80 format-3 | xxd -r -p >format-1
echo -n Salted__ | cat - key-slot-2 | openssl aes-256-cbc -d -md md5 -pass pass:pRiNcEsS -out mpassphrase
openssl aes-256-cbc -d -md md5 -pass fd:3 3<mpassphrase -in format-1 

Unit tests

As part of this task, write unit tests relying on openssl to validate encryption/decryption results. Let's assume some environment variable like NP2E_OPENSSL pointing to openssl.exe is set on the test system.

ProgerXP avatar Dec 16 '18 11:12 ProgerXP