diff --git a/.env.example b/.env.example
index 57bd3de..cf14c7a 100644
--- a/.env.example
+++ b/.env.example
@@ -101,6 +101,11 @@ APP_URL=https://your-domain.com
# TURNSTILE_SECRET_KEY=your-secret-key
JWT_SECRET=generate-a-random-64-char-string-here
+# Application-layer encryption key for PHI at rest (Nextcloud tokens, audio backups)
+# Generate with: openssl rand -hex 32
+# REQUIRED in production. Rotating invalidates existing encrypted data.
+DATA_ENCRYPTION_KEY=generate-with-openssl-rand-hex-32
+
# Email (for verification & password reset)
SMTP_HOST=smtp.gmail.com
SMTP_PORT=587
diff --git a/package.json b/package.json
index 4baa332..7deb5bf 100644
--- a/package.json
+++ b/package.json
@@ -17,6 +17,7 @@
"@tiptap/extension-underline": "^3.20.4",
"@tiptap/starter-kit": "^3.20.4",
"axios": "^1.7.7",
+ "argon2": "^0.41.1",
"bcryptjs": "^2.4.3",
"cookie-parser": "^1.4.7",
"cors": "^2.8.5",
diff --git a/public/index.html b/public/index.html
index 969d628..e1104db 100644
--- a/public/index.html
+++ b/public/index.html
@@ -9,8 +9,12 @@
-
+
+
@@ -412,7 +416,9 @@
-
+
diff --git a/public/js/learningHub.js b/public/js/learningHub.js
index 7f9fb2e..8b37414 100644
--- a/public/js/learningHub.js
+++ b/public/js/learningHub.js
@@ -1789,55 +1789,24 @@
}
function sanitizeHtml(html) {
- // Allowlist-based sanitizer: only safe tags and no event handlers
- var ALLOWED_TAGS = ['p','br','b','strong','i','em','u','s','h1','h2','h3','h4','h5','h6',
- 'ul','ol','li','a','blockquote','code','pre','table','thead','tbody','tr','th','td',
- 'hr','div','span','sub','sup','dl','dt','dd'];
- var ALLOWED_ATTRS = { 'a': ['href'], 'td': ['colspan','rowspan'], 'th': ['colspan','rowspan'], 'li': ['class'], 'ol': ['class'], 'ul': ['class'], 'span': ['class'], 'p': ['class'] };
-
- var div = document.createElement('div');
- div.innerHTML = html;
-
- function clean(node) {
- var children = Array.prototype.slice.call(node.childNodes);
- for (var i = 0; i < children.length; i++) {
- var child = children[i];
- if (child.nodeType === 3) continue; // text nodes OK
- if (child.nodeType !== 1) { child.remove(); continue; } // remove comments etc
-
- var tag = child.tagName.toLowerCase();
- if (ALLOWED_TAGS.indexOf(tag) === -1) {
- // Replace with text content
- var text = document.createTextNode(child.textContent);
- node.replaceChild(text, child);
- continue;
- }
-
- // Remove all attributes except explicitly allowed ones
- var allowed = ALLOWED_ATTRS[tag] || [];
- var attrs = Array.prototype.slice.call(child.attributes);
- for (var j = 0; j < attrs.length; j++) {
- if (allowed.indexOf(attrs[j].name) === -1) {
- child.removeAttribute(attrs[j].name);
- }
- }
-
- // For tags, validate href is not javascript:
- if (tag === 'a') {
- var href = (child.getAttribute('href') || '').trim().toLowerCase();
- if (href.indexOf('javascript:') === 0 || href.indexOf('data:') === 0 || href.indexOf('vbscript:') === 0) {
- child.setAttribute('href', '#');
- }
- child.setAttribute('rel', 'noopener noreferrer');
- child.setAttribute('target', '_blank');
- }
-
- clean(child);
- }
+ // DOMPurify is loaded from cdnjs via