From 990ead4285d7c55a0dae225132157782f944c128 Mon Sep 17 00:00:00 2001 From: talakbairam <47856077+talakbairam@users.noreply.github.com> Date: Wed, 21 Oct 2026 00:08:43 +0300 Subject: [PATCH] Add Copy as Encrypted Text and Decrypt Text from Clipboard --- CLAUDE.md | 2 +- README.md | 2 ++ package.json | 22 ++++++++++++ src/commands/encryptionCommands.ts | 47 ++++++++++++++++++++++++- src/constants/commands.ts | 2 ++ src/constants/strings.ts | 6 ++++ src/services/NoteEncryptionService.ts | 49 +++++++++++++++++++++++++++ 7 files changed, 128 insertions(+), 2 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index ce47849..d7d549e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -74,7 +74,7 @@ media/ quickNote.css/js, noteroot.svg (Activity Bar), icon.png, walk - **Move to ▸** (submenu `noteRoot.moveToMenu`, label "NoteRoot: Move to", declared in `contributes.submenus`; it is a **static** submenu with two fixed commands, because VS Code cannot put a changing list into a menu): `noteRoot.moveToQuickNote` ("Quick Note", `QuickNoteService.append`) and `noteRoot.moveTo` ("Choose Note…", the list below); both have the category "NoteRoot: Move to" so the Command Palette reads "NoteRoot: Move to: …", and both go through `cutSelectionTo(chooseDestination)` in `fileCommands.ts`. `noteRoot.moveTo` is cut and paste of the **selected text** of the active editor into another note (not a move of the file). It needs a non-empty selection in a file inside the notes folder (`pathGuard.isInsideRoot`, else an information message from `Strings.moveSelection`), asks for the destination with `pickDestination` in the same file: one QuickPick with **New note…** on top (then an input box checked by `QuickNoteService.validateTargetPath`, `normalizeTargetPath`) the notes of the Recent panel that still exist as files of the list (`RecentNotesService.list()`, newest first, under a separator "Recent"; `recentService` is a dependency of `registerFileCommands`) and then every other note file of the notes folder under "All notes" (`SearchService.walkFiles`, `isNoteFile` with the folder's `noteFileTypes`, the note being cut from left out, at most `MAX_MOVE_TARGETS` = 2000, sorted naturally; label = file name, description = folder, `matchOnDescription`), no open dialog; `chooseWithPreview` builds it with `createQuickPick` and follows `onDidChangeActive`: the highlighted note is shown with `showTextDocument(uri, { viewColumn: Beside, preview: true, preserveFocus: true, selection: end of the file })` (not for a file bigger than `maxTextFileBytes`; a newer highlight cancels an older one with a ticket), and at the end the tabs this command opened are closed with `closeTabsOf` (a tab that was open before stays); the mouse pointer alone does not change the highlight in a QuickPick, so the preview follows the keyboard and clicks, not a plain hover; it refuses the note it is cut from, then `QuickNoteService.appendText(relativePath, text)`: queued with the Quick Notes, writes the text **as it is** at the end (a blank line before it, no heading, no tag line, the file's own line ending, the file is created if new; an open editor gets a `WorkspaceEdit` and a clean file is saved again, like Quick Notes). Several selections are joined with a blank line. **The text is removed from the source only after the append worked** and only if the source did not change meanwhile (`version` equal, or every range still holds the same text), with a `WorkspaceEdit` that Ctrl+Z undoes; otherwise it stays and `moveSelection.changedMeanwhile` explains. Menus: `editor/context` with `noteRoot.activeInRoot && editorHasSelection && !editorReadonly`, and the Command Palette with `noteRoot.activeInRoot && editorHasSelection`; `ActiveNoteService` keeps the key `noteRoot.activeInRoot` (true while the active tab is a file inside the root that the exclude patterns do not hide; updated on active editor, tab, root, state and exclude changes). (A first version moved the whole file; that was a misunderstanding and was replaced.) - **Note header** (`noteRoot.noteHeader` is **folder-only**: `custom.folderSettings.noteHeader`, a list of lines, `isValidHeader`: at most `MAX_HEADER_LINES` 20 lines of at most 200 characters, no line breaks; the default is the comment ``, an empty list means no header; a `list` row in group Notes with `allowDuplicates` and `normalizeEntry` that keeps the spaces): pure `NoteHeader.ts` (`headerBlock`, `headerFields` = the names before the colon of each line, `hasHeader` = the file starts, after BOM and front matter, with an HTML comment that has every field name, whatever is written after them; a template without fields is compared line by line, `planHeader(existing, lines, eol)` = `{ offset, text }` or undefined: the block, then one blank line before old content, after BOM and front matter but **before** any other comment). `startOfNote` moved to `NoteStart.ts` (re-exported by `QuickNoteService`) and now also skips a leading `` block (returns it as `comment`; `skipComment = false` stops before it), so `planTagLine`, `findTopTagLine` and `buildStartInsertion` put tags and Quick Notes **below** the header. Writing: `FileOperationsService.createFile` writes the header block into every new file (line ending from `files.eol`, `util/lineEnding.ts`); `QuickNoteService.appendToFile` and `appendToOpenDocument` start a **new or empty** file with it before the first note (so also `appendText` of Move to); `NoteHeaderService.insertHeader(uri)` (`added`, `present`, `skipped`, `off`) is the command for existing notes: `noteRoot.insertHeader` (tree context menu of files, group `2_open@6`, hidden from the palette) and `noteRoot.insertHeaderCurrentNote` (palette), in `headerCommands.ts`; it edits an open document with a `WorkspaceEdit` (saved again only if it was clean, `InternalSaves`) and otherwise writes with `workspace.fs`. Existing files are never changed by themselves. - **Add ▸** (submenu `noteRoot.addMenu`, label "NoteRoot: Add", in `editor/context` with `noteRoot.activeInRoot && !editorReadonly`; static, two commands with the category "NoteRoot: Add", so the palette reads "NoteRoot: Add: …"): `noteRoot.insertHeaderCurrentNote` ("Header", the header of the **Note header** setting at the top of the active note, `NoteHeaderService.insertHeader`) and `noteRoot.addDateAndTags` ("Date and Tags", `headerCommands.ts`): `buildEntryHead` (exported from `QuickNoteService.ts`, the part of a Quick Note before its text: heading with `quickNoteTimestampFormat` and `quickNoteHeadingLevel`, blank line, tag line from `quickNoteTagLine` with **no tags**, i.e. empty when the template has text of its own, blank line; `buildBlock` uses it too) is inserted by `planEntryInsertion(lineIsBlank, isLastLine, head, eol)` from `NoteHeader.ts`: at the start of an empty line, or at the end of a line with text after a blank line (before more text the block loses its last line ending, so one blank line follows it); the cursor ends right after the block. With no date format and a tag line template that is only `{tags}` there is nothing to add and a message says so. Nothing is saved: it is an editor change that Ctrl+Z undoes. -- **Encryption** (`NoteCrypto.ts` pure: `encryptBytes` / `decryptToBytes`, AES-256-GCM, scrypt N=2^15 r=8 p=1, salt 16 / iv 12 / tag 16, header line `NOTEROOT-ENCRYPTED v1` as AAD, base64 lines of 76; key normalized NFC, `MIN_KEY_LENGTH` 8; wrong key and tampering are both `WrongKeyError`). `NoteEncryptionService` (no UI): `encrypt` refuses `.enc` names, the Quick Note file, unsaved open documents, binary or over 10 MB files and a taken `.enc`; it decrypts its own result and compares, writes it into the original file, reads it back, then renames to `.enc` with `FileOperationsService.renameKeepingEditors` (now public; pins and recents follow). `decryptPermanently` renames first, then writes the plaintext (crash-safe order). `decryptForViewing` returns the text; `DecryptedViewService` shows it in a read-only `noteroot-decrypted:` document (content provider, text dropped on `onDidCloseTextDocument`). Commands in `encryptionCommands.ts` (`encryptNote` asks the key twice with `password: true`, `decryptView`, `decryptNote`; several files share a key). `NoteItem` adds the context suffix `.locked`, a `lock` icon and the click command `decryptView` for `.enc` files; menus group `7_encrypt`, and Run Tagging / Insert Header are hidden for `.locked`. Guards: `SearchService.readText` returns undefined for `.enc` (so search, tag scan, tagging and headers skip them), `NoteTaggingService` skips them, `QuickNoteService.validateTargetPath` / `writeText` refuse them. A link (symbolic) is refused, because a write would go through it to a file that may be outside the root. `decryptPermanently` gives the file its `.enc` name back if writing the plain text fails. `openFile` (every way of opening a file from the tree, search, Recent and Pinned) sends `.enc` files to `noteRoot.decryptView` instead of the editor. `tools/decrypt.js` (not shipped in the `.vsix`) decrypts a note with Node alone and `tools/decrypt.ps1` does the same in Windows PowerShell 5.1 or 7 with nothing installed (scrypt and AES-GCM are written in C# inside the script, because .NET Framework has neither); both must stay in step with `NoteCrypto.ts` (same layout, scrypt parameters and AAD). The key is never stored or logged. +- **Encryption** (`NoteCrypto.ts` pure: `encryptBytes` / `decryptToBytes`, AES-256-GCM, scrypt N=2^15 r=8 p=1, salt 16 / iv 12 / tag 16, header line `NOTEROOT-ENCRYPTED v1` as AAD, base64 lines of 76; key normalized NFC, `MIN_KEY_LENGTH` 8; wrong key and tampering are both `WrongKeyError`). `NoteEncryptionService` (no UI): `encrypt` refuses `.enc` names, the Quick Note file, unsaved open documents, binary or over 10 MB files and a taken `.enc`; it decrypts its own result and compares, writes it into the original file, reads it back, then renames to `.enc` with `FileOperationsService.renameKeepingEditors` (now public; pins and recents follow). `decryptPermanently` renames first, then writes the plaintext (crash-safe order). `decryptForViewing` returns the text; `DecryptedViewService` shows it in a read-only `noteroot-decrypted:` document (content provider, text dropped on `onDidCloseTextDocument`). Commands in `encryptionCommands.ts` (`encryptNote` asks the key twice with `password: true`, `decryptView`, `decryptNote`; several files share a key). `NoteItem` adds the context suffix `.locked`, a `lock` icon and the click command `decryptView` for `.enc` files; menus group `7_encrypt`, and Run Tagging / Insert Header are hidden for `.locked`. Guards: `SearchService.readText` returns undefined for `.enc` (so search, tag scan, tagging and headers skip them), `NoteTaggingService` skips them, `QuickNoteService.validateTargetPath` / `writeText` refuse them. A link (symbolic) is refused, because a write would go through it to a file that may be outside the root. `decryptPermanently` gives the file its `.enc` name back if writing the plain text fails. `openFile` (every way of opening a file from the tree, search, Recent and Pinned) sends `.enc` files to `noteRoot.decryptView` instead of the editor. `tools/decrypt.js` (not shipped in the `.vsix`) decrypts a note with Node alone and `tools/decrypt.ps1` does the same in Windows PowerShell 5.1 or 7 with nothing installed (scrypt and AES-GCM are written in C# inside the script, because .NET Framework has neither); both must stay in step with `NoteCrypto.ts` (same layout, scrypt parameters and AAD). The key is never stored or logged. **Sending a note as text:** `noteRoot.copyEncrypted` (tree context menu of non-locked files, group `7_encrypt@3`) calls `NoteEncryptionService.encryptedTextOf` (reads the open editor text if the note is open, else the file; same refusals and round-trip check as `encrypt`; writes nothing) and puts the `NOTEROOT-ENCRYPTED v1` text on the clipboard; `noteRoot.decryptClipboard` (palette and the `...` menu of the Notes view, group `3_encrypt@1`) reads the clipboard, checks `looksEncrypted` on the trimmed text, asks the key and shows `decryptText` in the read-only `noteroot-decrypted:` document (`Strings.encryption.clipboardName`). - **Show Hidden Items** (two commands that swap, like the Advanced mode: `noteRoot.showHiddenItems` "Show Hidden Items" while the context key `noteRoot.showHidden` is false and `noteRoot.hideHiddenItems` "Hide Hidden Items" while it is true, in the `...` menu of the Notes view, group `1_view@1`, and in the Command Palette; a contributed command cannot change its own title, and the `toggled` check mark was dropped): `ShowHiddenService` keeps the on/off state in `globalState` (`noteRoot.showHidden`, per machine), publishes the context key of the same name and fires `onDidChange`, which makes `NotesTreeProvider` refresh (it takes the service as its **last** constructor argument). In `getChildren` an entry that `ExcludeMatcher` hides is left out, unless the switch is on and the entry is not the config folder `.noteroot` (always out of sight); a shown one becomes a `NoteItem` with `hidden = true` (description "hidden"), and so does everything below a hidden folder (`element.hidden`). Only the tree changes: `SearchService`, `TagIndexService`, `AutoCategoryService`, Recent and Pinned keep honouring the exclude patterns. - **Show in NoteRoot Again** (`noteRoot.includeItem`, tree context menu of **hidden** items only, `fileCommands.ts`): `ExcludeMatcher.matchingPatterns(name, relativePath)` gives the patterns of the list that hide an entry (`Rule` keeps the raw `pattern` and `builtin`, the config folder rule, which is never listed). `NoteItem` adds the suffix `.hidden` to its context value when `hidden`, so the menus test the start (`^noteRoot.(file|folder)`) and Exclude is shown with a negative-lookahead regex (there is no `!~` in `when` clauses), Include with `=~ /\.hidden$/`. For each hidden item: a pattern that is exactly `/` (any case) is taken out of the effective list at once; a pattern that matches more (`.git`, `*.tmp`) needs a modal confirmation (`Remove Pattern`); an item with no pattern of its own is hidden by a folder above it, and a message offers to include that folder (`Include the Folder`, only when that folder has an exact `/path` pattern). The list is written with `settings.setValue(excludePatterns, ...)` (folder list) and the message has Undo. - **Don't Auto-Tag / Auto-Tag Again** (`noteRoot.excludeFromTagging` / `noteRoot.includeInTagging`, tree context menu of files and folders, `taggingCommands.ts`; palette hidden): the folder-only setting `noTagPatterns` (`custom.folderSettings.noTagPatterns`, list of patterns of the same syntax as the hidden list, default empty; row **Files and folders without tagging** in group Tags & Categories) read by a second `ExcludeMatcher` (`new ExcludeMatcher(settings, { key, read, always: [] })`: the constructor takes the setting key, a reader and the names that always match, `.noteroot` only for the hidden list), `noTagMatcher` in `extension.ts`. `NoteTaggingService` takes it as its **last** constructor argument: `tagNote` returns `{ status: "excluded" }` (before reading the file) and `planOnSave` returns undefined, for a note or a folder above it that the list matches (`isExcludedFromTagging`, every segment, like `isHidden`). Quick Note creation and the tags the user chooses are not affected (only automatic tagging). The tree: `NoteItem` has `noTag` (the matcher says so, or the folder it is in is `element.noTag`), description "no tags" ("hidden · no tags" with both) **only while the Show Hidden switch is on** (`markNoTag`, passed by the provider from `ShowHiddenService`), context value suffix `.notag` after `.hidden` **always** (so the menu knows Include from Exclude); the menus test them anywhere in the value; the "not" side (Exclude) is a positive regex with a negative lookahead, `/^noteRoot\.(file|folder)(?!.*\.notag)/`, because **when clauses have no `!~` operator** (with it the Exclude entries were always shown); the same for `.hidden`. The adding and taking out are the generic `addToPatternList` / `removeFromPatternList` of `patternLists.ts` (`PatternListOptions`: setting key, reader, matcher and the texts), which the hidden list uses too (Exclude/Include in `fileCommands.ts`). diff --git a/README.md b/README.md index 1df9581..acc2fd2 100644 --- a/README.md +++ b/README.md @@ -78,6 +78,8 @@ Open the **Actions** panel and choose **Connect to a Sync Service**. VS Code sig Right-click a note and choose **Encrypt…**, then type a master key twice. The note is renamed `name.ext.enc` and shows a lock. Click it (or **Decrypt for Viewing…**) to read it in a read-only editor that exists only in memory; **Decrypt Permanently…** restores the note. +To send a note to someone, right-click it and choose **Copy as Encrypted Text…**: the encrypted text goes to the clipboard and the note stays as it is. The other person copies it and runs **NoteRoot: Decrypt Text from Clipboard…** (Command Palette, or the `...` menu of the Notes view; they need NoteRoot and the key). + - AES-256-GCM, with a key from your master key by scrypt. Tampering is detected. The file name and size stay visible. - **The key is never stored and cannot be recovered. If you forget it, the note is lost.** Old copies (Local History, Git, backups) may still hold the plain text. - Without the extension, `tools/decrypt.js` (Node.js) or `tools/decrypt.ps1` (PowerShell on Windows) decrypt a note. No independent security review yet. diff --git a/package.json b/package.json index 7a224b3..885d9fd 100644 --- a/package.json +++ b/package.json @@ -376,6 +376,14 @@ "command": "noteRoot.decryptNote", "title": "Decrypt Permanently…" }, + { + "command": "noteRoot.copyEncrypted", + "title": "Copy as Encrypted Text…" + }, + { + "command": "noteRoot.decryptClipboard", + "title": "NoteRoot: Decrypt Text from Clipboard…" + }, { "command": "noteRoot.tagNote", "title": "Run Tagging" @@ -631,6 +639,11 @@ "when": "view == noteRoot.tree && noteRoot.state == ready", "group": "1_edit@1" }, + { + "command": "noteRoot.decryptClipboard", + "when": "view == noteRoot.tree", + "group": "3_encrypt@1" + }, { "command": "noteRoot.quickNoteOptions", "when": "view == noteRoot.quickNote", @@ -740,6 +753,10 @@ "command": "noteRoot.decryptNote", "when": "false" }, + { + "command": "noteRoot.copyEncrypted", + "when": "false" + }, { "command": "noteRoot.recreateRootFolder", "when": "false" @@ -1246,6 +1263,11 @@ "when": "view == noteRoot.tree && viewItem =~ /^noteRoot\\.file(?!.*\\.(locked|quickNote))/", "group": "7_encrypt@1" }, + { + "command": "noteRoot.copyEncrypted", + "when": "view == noteRoot.tree && viewItem =~ /^noteRoot\\.file(?!.*\\.locked)/", + "group": "7_encrypt@3" + }, { "command": "noteRoot.decryptView", "when": "view == noteRoot.tree && viewItem =~ /^noteRoot\\.file.*\\.locked/", diff --git a/src/commands/encryptionCommands.ts b/src/commands/encryptionCommands.ts index a2a18e9..e3ec78e 100644 --- a/src/commands/encryptionCommands.ts +++ b/src/commands/encryptionCommands.ts @@ -3,7 +3,7 @@ import { Commands } from "../constants/commands"; import { Strings } from "../constants/strings"; import { DecryptedViewService } from "../services/DecryptedViewService"; import { Logger } from "../services/Logger"; -import { MIN_KEY_LENGTH, isEncryptedName } from "../services/NoteCrypto"; +import { MIN_KEY_LENGTH, isEncryptedName, looksEncrypted } from "../services/NoteCrypto"; import { NoteEncryptionService } from "../services/NoteEncryptionService"; import { PathGuard } from "../services/PathGuard"; import { NoteItem } from "../tree/NoteItem"; @@ -166,6 +166,51 @@ export function registerEncryptionCommands(deps: EncryptionCommandDependencies): } ); + // The note is not changed: its encrypted text goes to the clipboard, to be sent to someone who has the key. + registerSafeCommand( + context, + logger, + Commands.copyEncrypted, + async (clicked?: unknown, selected?: unknown) => { + const item = filesOf(clicked, selected).find((entry) => !isEncryptedName(entry.name)); + if (!item) { + return; + } + const key = await askNewKey(text.copyTitle); + if (key === undefined) { + return; + } + const encrypted = await vscode.window.withProgress( + { location: vscode.ProgressLocation.Notification, title: text.working }, + () => encryptionService.encryptedTextOf(item.uri, key) + ); + await vscode.env.clipboard.writeText(encrypted); + showStatus(text.copied(item.name)); + } + ); + + // The other side: encrypted text from the clipboard is shown for reading (a read-only document, nothing is written). + registerSafeCommand(context, logger, Commands.decryptClipboard, async () => { + const clipboard = await vscode.env.clipboard.readText(); + if (!looksEncrypted(`${clipboard.trim()}\n`)) { + void vscode.window.showWarningMessage(text.clipboardNotEncrypted); + return; + } + const key = await askKey(text.clipboardTitle); + if (key === undefined) { + return; + } + try { + const plain = await encryptionService.decryptText(clipboard, key); + const document = await vscode.workspace.openTextDocument( + decryptedView.add(text.clipboardName, plain) + ); + await vscode.window.showTextDocument(document, { preview: false }); + } catch (error) { + reportError(logger, "Decrypt clipboard", error); + } + }); + // A click on an encrypted note in the tree runs this too (the argument is then the address of the file). registerSafeCommand(context, logger, Commands.decryptView, async (target?: unknown) => { const uri = diff --git a/src/constants/commands.ts b/src/constants/commands.ts index d32f042..a6d556f 100644 --- a/src/constants/commands.ts +++ b/src/constants/commands.ts @@ -22,6 +22,8 @@ export const Commands = { encryptNote: "noteRoot.encryptNote", decryptView: "noteRoot.decryptView", decryptNote: "noteRoot.decryptNote", + copyEncrypted: "noteRoot.copyEncrypted", + decryptClipboard: "noteRoot.decryptClipboard", excludeFromTagging: "noteRoot.excludeFromTagging", includeInTagging: "noteRoot.includeInTagging", showHiddenItems: "noteRoot.showHiddenItems", diff --git a/src/constants/strings.ts b/src/constants/strings.ts index 2cdd9d3..e6c7013 100644 --- a/src/constants/strings.ts +++ b/src/constants/strings.ts @@ -708,6 +708,12 @@ export const Strings = { decrypted: (name: string, count: number): string => count === 1 ? `${name} is decrypted.` : `${count} notes are decrypted.`, working: "Working…", + copyTitle: "Copy this note as encrypted text", + copied: (name: string): string => + `The encrypted text of ${name} is on the clipboard. The note itself was not changed.`, + clipboardTitle: "Decrypt the text on the clipboard", + clipboardNotEncrypted: "The clipboard does not hold encrypted NoteRoot text.", + clipboardName: "Decrypted text.md", isLink: (name: string): string => `${name} is a link, so it was left as it is (writing would change the file it points to).`, cannotWriteInto: "An encrypted note cannot be written to. Decrypt it first.", diff --git a/src/services/NoteEncryptionService.ts b/src/services/NoteEncryptionService.ts index cb8610f..df371ac 100644 --- a/src/services/NoteEncryptionService.ts +++ b/src/services/NoteEncryptionService.ts @@ -122,6 +122,55 @@ export class NoteEncryptionService { return { name: decryptedName(name), text: decoder.decode(plain) }; } + /** + * The text of a note, encrypted, to be copied and sent. Nothing is written: the note stays as it is. An open document is read + * from the editor, so what is seen is what is encrypted. + * @throws EncryptionRefusedError for a file that is not text, too big, or encrypted already. + */ + async encryptedTextOf(uri: vscode.Uri, key: string): Promise { + this.pathGuard.assertInsideRoot(uri, { allowRoot: false }); + const name = basename(uri); + if (isEncryptedName(name)) { + throw new EncryptionRefusedError(Strings.encryption.alreadyEncrypted(name)); + } + const open = vscode.workspace.textDocuments.find((doc) => this.pathGuard.isSame(doc.uri, uri)); + let bytes: Uint8Array; + if (open) { + bytes = encoder.encode(open.getText()); + } else { + const stat = await vscode.workspace.fs.stat(uri); + if (stat.size > MAX_ENCRYPTED_BYTES) { + throw new EncryptionRefusedError(Strings.encryption.tooBig(name)); + } + bytes = await vscode.workspace.fs.readFile(uri); + } + if (bytes.length > MAX_ENCRYPTED_BYTES) { + throw new EncryptionRefusedError(Strings.encryption.tooBig(name)); + } + if (bytes.subarray(0, 8000).includes(0)) { + throw new EncryptionRefusedError(Strings.encryption.notText(name)); + } + if (looksEncrypted(decoder.decode(bytes.subarray(0, 64)))) { + throw new EncryptionRefusedError(Strings.encryption.alreadyEncrypted(name)); + } + const encrypted = await encryptBytes(bytes, key); + const check = await decryptToBytes(encrypted, key, name); + if (check.length !== bytes.length || !check.every((byte, index) => byte === bytes[index])) { + throw new Error("The encrypted text did not decrypt to the original."); + } + this.logger.info(`Copied encrypted text of ${this.pathGuard.getRelativePath(uri) ?? name}.`); + return encrypted; + } + + /** + * Decrypts encrypted text that did not come from a file (pasted or copied from a message). White space around it is ignored. + * @throws NotEncryptedError / WrongKeyError. + */ + async decryptText(encrypted: string, key: string): Promise { + const plain = await decryptToBytes(`${encrypted.trim()}\n`, key); + return decoder.decode(plain); + } + private async decryptFile(uri: vscode.Uri, key: string, name: string): Promise { const stat = await vscode.workspace.fs.stat(uri); if (stat.size > MAX_ENCRYPTED_BYTES * 2) {