Skip to content

Commit 6c602f5

Browse files
committed
adds markdown caching and refactored decryption
1 parent c00667e commit 6c602f5

File tree

7 files changed

+99
-113
lines changed

7 files changed

+99
-113
lines changed

src/main/resources/public/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
"dropzone": "^6.0.0-beta.1",
2727
"emoji-picker-react": "^4.4.9",
2828
"fullcalendar": "^6.1.8",
29+
"quick-lru": "^7.0.0",
2930
"react": "^18.2.0",
3031
"react-dom": "^18.2.0",
3132
"react-emoji-render": "^2.0.1",

src/main/resources/public/src/Crypto.ts

Lines changed: 57 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import * as J from "./JavaIntf";
33
import { S } from "./Singletons";
44
import { Constants as C } from "./Constants";
55
import { NodeInfo, PrincipalName } from "./JavaIntf";
6+
import { dispatch } from "./AppContext";
67

78
/*
89
SYMMETRIC ENCRYPTION and PUBLIC KEY ENCRYPTION
@@ -81,6 +82,13 @@ export class Crypto {
8182
asymEncKey: string = null;
8283
userSignature: string = null;
8384

85+
// Set of nodeId to NodeInfo of all nodes pending decryption. Don't be tempted to make this a map with perhaps nodeId
86+
// as key becasue we can have various different NodeInfo objects that are the same node, so we need to use the NodeInfo
87+
// object itself. Also we already have a clearTextCache so performance is not an issue.
88+
pendingDecrypt: Set<NodeInfo> = new Set<NodeInfo>();
89+
90+
clearTextCache: Map<string, string> = new Map<string, string>();
91+
8492
constructor() {
8593
/* WARNING: Crypto (or at least subtle) will not be available except on Secure Origin, which means a SSL (https)
8694
web address plus also localhost */
@@ -104,6 +112,53 @@ export class Crypto {
104112
this.vector = new Uint8Array([71, 73, 79, 83, 89, 37, 41, 47, 53, 67, 97, 103, 107, 109, 127, 131]);
105113
}
106114

115+
queueDecrypt(node: NodeInfo) {
116+
if (S.props.isEncrypted(node)) {
117+
// if we have already decrypted this content then just use the decrypted content
118+
const content = this.clearTextCache.get(node.content);
119+
if (content) {
120+
node.content = content;
121+
return;
122+
}
123+
// else we add to pending decrypts
124+
this.pendingDecrypt.add(node);
125+
}
126+
}
127+
128+
async decryptAll() {
129+
if (this.pendingDecrypt.size === 0) return;
130+
for (const node of this.pendingDecrypt) {
131+
await this.decryptNode(node);
132+
}
133+
this.pendingDecrypt.clear();
134+
dispatch("afterDecryptAll", _s => { });
135+
}
136+
137+
async decryptNode(node: NodeInfo) {
138+
if (!this.avail) return;
139+
let clearText = null;
140+
141+
if (node.content.startsWith(J.Constant.ENC_TAG)) {
142+
// check if we have decrypted this content before
143+
clearText = this.clearTextCache.get(node.content);
144+
145+
// if now, then decrypt it now
146+
if (!clearText) {
147+
const cipherText = node.content.substring(J.Constant.ENC_TAG.length);
148+
const cipherKey = S.props.getCryptoKey(node);
149+
if (cipherKey) {
150+
clearText = await S.crypto.decryptSharableString(null, { cipherKey, cipherText });
151+
this.clearTextCache.set(node.content, clearText);
152+
}
153+
}
154+
}
155+
156+
// console.log("Decrypted to " + clearText);
157+
// Warning clearText can be "" (which is a 'falsy' value and a valid decrypted string!)
158+
clearText = clearText !== null ? clearText : "[Decrypt Failed]";
159+
node.content = clearText;
160+
}
161+
107162
invalidateKeys() {
108163
console.log("Setting crypto keys to all null");
109164
this.sigKey = null;
@@ -704,16 +759,6 @@ export class Crypto {
704759

705760
/* Inverse of encryptSharableString() function */
706761
async decryptSharableString(privateKey: CryptoKey, skpd: SymKeyDataPackage): Promise<string> {
707-
// get hash of the encrypted data
708-
const cipherHash: string = S.util.hashOfString(skpd.cipherText);
709-
710-
let ret = S.quanta.decryptCache.get(cipherHash);
711-
// if we have already decrypted this data return the result.
712-
if (ret) {
713-
// decryption cache hit
714-
return ret;
715-
}
716-
717762
try {
718763
// console.log("decrypting [" + skpd.cipherText + "] with cipherKey: " + skpd.cipherKey);
719764
privateKey = privateKey || await this.getPrivateEncKey();
@@ -731,8 +776,7 @@ export class Crypto {
731776

732777
const symKeyJsonObj: JsonWebKey = JSON.parse(symKeyJsonStr);
733778
const symKey = await crypto.subtle.importKey("jwk", symKeyJsonObj, this.SYM_ALGO, true, this.OP_ENC_DEC);
734-
ret = await this.symDecryptString(symKey, skpd.cipherText);
735-
S.quanta.decryptCache.set(cipherHash, ret);
779+
const ret = await this.symDecryptString(symKey, skpd.cipherText);
736780
return ret;
737781
}
738782
catch (ex) {
@@ -835,7 +879,7 @@ export class Crypto {
835879

836880
let path: string = node.path;
837881
// convert any 'pending (p)' path to a final version of the path (no '/p/')
838-
if (path.startsWith("/r/p/")) {
882+
if (path.startsWith("/r/p/")) {
839883
path = "/r/" + path.substring(5);
840884
}
841885

src/main/resources/public/src/NodeUtil.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -365,6 +365,7 @@ export class NodeUtil {
365365
}
366366
}
367367
}
368+
S.crypto.queueDecrypt(node);
368369

369370
if (node.children) {
370371
this.processInboundNodes(node.children);

src/main/resources/public/src/Quanta.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -71,10 +71,6 @@ export class Quanta {
7171
ctrlKey: boolean;
7272
ctrlKeyTime: number;
7373

74-
// maps the hash of an encrypted block of text to the unencrypted text, so that we never run the same
75-
// decryption code twice.
76-
decryptCache: Map<string, string> = new Map<string, string>();
77-
7874
/* Map of all URLs and the openGraph object retrieved for it */
7975
openGraphData: Map<string, J.OpenGraph> = new Map<string, J.OpenGraph>();
8076
imageUrls: Set<string> = new Set<string>();
@@ -220,6 +216,7 @@ export class Quanta {
220216
window.onpopstate = this._onPopState;
221217

222218
this.addPageLevelEventListeners();
219+
this.setDecryptionTimer();
223220
Log.log("initConstants");
224221
S.props.initConstants();
225222

@@ -380,6 +377,14 @@ export class Quanta {
380377
return window.innerWidth > window.innerHeight;
381378
}
382379

380+
setDecryptionTimer() {
381+
setInterval(() => {
382+
if (S.crypto.pendingDecrypt.size > 0) {
383+
S.crypto.decryptAll();
384+
}
385+
}, 2000);
386+
}
387+
383388
addPageLevelEventListeners() {
384389
/* We have to run this timer to wait for document.body to exist because we load our JS in
385390
the HTML HEAD because we need our styling in place BEFORE the page renders or else you

src/main/resources/public/src/comp/node/NodeCompMarkdown.ts

Lines changed: 26 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,12 @@ import { Comp, CompT } from "../base/Comp";
77
import ReactMarkdownComp from "../core/ReactMarkdownComp";
88
import { NodeInfo } from "../../JavaIntf";
99
import { UrlInfo } from "../../plugins/base/TypeBase";
10+
import QuickLRU from 'quick-lru';
11+
12+
const cache = new QuickLRU({ maxSize: 1000 });
1013

1114
interface LS {
1215
content: string;
13-
pendingDecrypt?: string;
1416
}
1517

1618
export class NodeCompMarkdown extends Comp {
@@ -28,48 +30,26 @@ export class NodeCompMarkdown extends Comp {
2830
// but is ok named as 'cont'
2931
cont: string;
3032

31-
/* This makes the encrypted text visible without editing the node which is important to have
32-
on so nodes shared to you can be seen, because a user can't edit nodes they don't own */
33-
private autoDecrypting: boolean = true;
34-
35-
constructor(public node: NodeInfo, extraContainerClass: string, _tabData: TabBase<any>, urls: Map<string, UrlInfo>) {
33+
constructor(public node: NodeInfo, extraContainerClass: string, _tabData: TabBase<any>, private urls: Map<string, UrlInfo>) {
3634
super({ key: "ncmkd_" + node.id });
3735
this.cont = node.renderContent || node.content;
38-
const ast = getAs();
3936
this.attribs.nodeId = node.id; // this 'nodeId' is needed to track expand collapse of code blocks.
4037
this.attribs.className = "mkCont";
4138

4239
if (extraContainerClass) {
4340
this.attribs.className += " " + extraContainerClass;
4441
}
4542

46-
const content = this.cont || "";
47-
const state: LS = {
48-
content: null
49-
};
50-
51-
/* If this content is encrypted we set it in 'pendingDecrypt' to decrypt it asynchronously */
52-
if (S.props.isEncrypted(node)) {
53-
state.content = "[Encrypted]";
54-
55-
if (!ast.isAnonUser) {
56-
state.pendingDecrypt = content;
57-
}
58-
}
59-
/* otherwise it's not encrypted and we display the normal way */
60-
else {
61-
state.content = this.preprocessMarkdown(node, null, urls);
62-
}
63-
64-
this.mergeState<LS>(state);
43+
this.mergeState<LS>({
44+
content: this.cont || ""
45+
});
6546
}
6647

6748
/* If content is passed in it will be used. It will only be passed in when the node is encrypted and the text
6849
has been decrypted and needs to be rendered, in which case we don't need the node.content, but use the 'content' parameter here */
69-
preprocessMarkdown(node: NodeInfo, content: string = null, urls: Map<string, UrlInfo>): string {
70-
content = content || this.cont || "";
71-
let val = "";
72-
val = S.render.injectSubstitutions(node, content);
50+
preprocessMarkdown(node: NodeInfo, urls: Map<string, UrlInfo>): string {
51+
const content = this.cont || "";
52+
let val = S.render.injectSubstitutions(node, content);
7353

7454
if (S.props.isMine(node)) {
7555
val = S.util.makeHtmlCommentsVisible(val);
@@ -103,52 +83,30 @@ export class NodeCompMarkdown extends Comp {
10383
.replaceAll("\\]", "$$");
10484
}
10585

106-
override preRender(): boolean | null {
107-
const state: LS = this.getState<LS>();
108-
109-
if (this.autoDecrypting && state.pendingDecrypt) {
110-
let cipherText = null;
111-
if (state.pendingDecrypt.startsWith(J.Constant.ENC_TAG)) {
112-
cipherText = state.pendingDecrypt.substring(J.Constant.ENC_TAG.length);
113-
}
114-
115-
if (!cipherText) {
116-
console.log("not decrypting. cipherText was unexpected format: " + cipherText);
117-
return;
118-
}
119-
120-
const cipherHash = S.util.hashOfString(cipherText);
121-
let clearText = S.quanta.decryptCache.get(cipherHash);
122-
// if we have already decrypted this data use the result.
123-
if (clearText) {
124-
clearText = this.preprocessMarkdown(this.node, clearText, null);
125-
126-
this.mergeState<LS>({
127-
content: clearText,
128-
pendingDecrypt: null
129-
});
130-
}
131-
else {
132-
setTimeout(() => {
133-
this.decrypt();
134-
}, 10);
135-
}
136-
}
137-
return true;
138-
}
139-
14086
override compRender(_children: CompT[]): ReactNode {
14187
const state = this.getState<LS>();
14288

14389
// ReactMarkdown can't have this 'ref' and would throw a warning if we did
14490
delete this.attribs.ref;
14591

92+
if (state.content?.indexOf(J.Constant.ENC_TAG) === 0) {
93+
return createElement(ReactMarkdownComp as any, this.attribs, "[Encrypted]");
94+
}
95+
96+
const key = this.attribs.key + "_" + state.content;
97+
let ret: any = cache.get(key);
98+
if (ret) {
99+
return cache.get(key) as ReactNode;
100+
}
101+
102+
const content = this.preprocessMarkdown(this.node, this.urls);
146103
// Process with special markdown if there is any.
147-
const sections = this.processSpecialMarkdown(state.content);
148-
if (sections) {
149-
return sections;
104+
ret = this.processSpecialMarkdown(content);
105+
if (!ret) {
106+
ret = createElement(ReactMarkdownComp as any, this.attribs, content);
150107
}
151-
return createElement(ReactMarkdownComp as any, this.attribs, state.content);
108+
cache.set(key, ret);
109+
return ret;
152110
}
153111

154112
/* When any markdown content contains something like "-**My Section Title**-" that will be
@@ -272,31 +230,4 @@ export class NodeCompMarkdown extends Comp {
272230
children.push(createElement(ReactMarkdownComp as any, attribs, curBuf));
273231
}
274232
}
275-
276-
async decrypt() {
277-
if (!S.crypto.avail) return;
278-
const state: LS = this.getState<LS>();
279-
if (!state.pendingDecrypt) return;
280-
let clearText = null;
281-
// console.log("decrypting (in NodeCompMarkdown): " + state.pendingDecrypt);
282-
283-
if (state.pendingDecrypt.startsWith(J.Constant.ENC_TAG)) {
284-
const cipherText = state.pendingDecrypt.substring(J.Constant.ENC_TAG.length);
285-
const cipherKey = S.props.getCryptoKey(this.node);
286-
if (cipherKey) {
287-
// console.log("CIPHERKEY " + cipherKey);
288-
clearText = await S.crypto.decryptSharableString(null, { cipherKey, cipherText });
289-
}
290-
}
291-
292-
// console.log("Decrypted to " + clearText);
293-
// Warning clearText can be "" (which is a 'falsy' value and a valid decrypted string!)
294-
clearText = clearText !== null ? clearText : "[Decrypt Failed]";
295-
clearText = this.preprocessMarkdown(this.node, clearText, null);
296-
297-
this.mergeState<LS>({
298-
content: clearText,
299-
pendingDecrypt: null
300-
});
301-
}
302233
}

src/main/resources/public/src/dlg/EditNodeDlg.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -937,7 +937,6 @@ export class EditNodeDlg extends DialogBase {
937937
}
938938
else {
939939
if (S.crypto.avail) {
940-
// console.log("decrypting: " + value);
941940
const cipherText = value.substring(J.Constant.ENC_TAG.length);
942941
const cipherKey = S.props.getCryptoKey(ast.editNode);
943942
if (cipherKey) {

src/main/resources/public/yarn.lock

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3334,6 +3334,11 @@ queue-microtask@^1.2.2:
33343334
resolved "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz"
33353335
integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==
33363336

3337+
quick-lru@^7.0.0:
3338+
version "7.0.0"
3339+
resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-7.0.0.tgz#447f6925b33ae4d2d637e211967d74bae4b99c3f"
3340+
integrity sha512-MX8gB7cVYTrYcFfAnfLlhRd0+Toyl8yX8uBx1MrX7K0jegiz9TumwOK27ldXrgDlHRdVi+MqU9Ssw6dr4BNreg==
3341+
33373342
react-dom@^18.2.0:
33383343
version "18.3.1"
33393344
resolved "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz"

0 commit comments

Comments
 (0)