PrivacyNotes.app — Security Assessment
Summary
Independent source review of the public PrivacyNotes.app client at commit 3d98da9. The assessment identified seven actionable Low/Medium client-side defects and one documented design limitation; no client-side cryptographic break was identified.
Maintainer response: The project acknowledges six defects and one documented limitation, with four fixes in v0.522.1. This report uses seven actionable finding identifiers and one limitation. A finding-by-finding correspondence and independent retest evidence have not been published here; the counts must not be treated as equivalent. Project acknowledgment.
Document record
- Author
- Eduardo Camarillo [Noir0x63]
- Published
- Version
- v1.0
- Language
- English
Download the source report (Markdown) · Project acknowledgment and remediation record
| Target | LifetimeLabsDev/PrivacyNotes.app |
| Commit assessed | 3d98da9 (v0.518) |
| Component | Publicly available source code at the pinned commit: web client (PWA), shared crypto package, and Tauri desktop shell. Server-side infrastructure not present in the public repository could not be assessed. |
| Assessment type | Independent, unsolicited open-source security review |
| Assessment date | September 18, 2026 |
| Researcher | Eduardo Camarillo |
1. Executive Summary
PrivacyNotes.app is a multi-platform end-to-end encrypted notes client (web PWA and Tauri desktop/mobile) that integrates with Supabase/Cloudflare-hosted backend services. According to the public client code and project documentation, a BIP-39 recovery phrase derives an Ed25519 identity, an XChaCha20-Poly1305 encryption key, and authentication credentials on-device; the backend is designed to receive ciphertext rather than note plaintext.
This was an independent, unsolicited review of the source code publicly available in LifetimeLabsDev/PrivacyNotes.app at the commit above. The assessment covered the web application, the shared cryptography package, and the Tauri desktop/mobile shell present in that repository. No private source, production credentials, internal documentation, vendor-provided access, or non-public infrastructure was available or assumed. Server-side infrastructure not present in the public repository (including Supabase RLS policies, server-side edge-function implementations, Paddle billing webhooks, and storage bucket policies) could therefore not be assessed; findings and conclusions below are scoped strictly to the publicly available code.
No client-side cryptographic break was identified in the reviewed source code. Direct execution of the project's own known-answer tests confirmed that the reviewed AEAD paths reject ciphertext tampering, nonce swapping, and wrong-key decryption (29/29 passing, including the property the codebase itself calls "server-cannot-lie"). No actionable undisclosed finding demonstrates compromise of note-content confidentiality, and no actionable undisclosed finding reaches High or Critical severity under the final report handling. The documented get-custodial-phrase bearer-only limitation carries higher conditional risk if scored separately under bearer-token theft, and is therefore presented separately from the actionable defect list.
The report identifies seven actionable client-side defects and one documented design limitation. The actionable issues are concentrated in integrity, availability, and metadata/local-storage privacy rather than in the E2EE design itself: one unclamped-timestamp integrity bug in backup restore, one local denial-of-service in ZIP import handling, one CSS sanitizer bypass enabling clickjacking-style redress, one silent-failure gap in the custodial-key opt-in flow, one missing data-loss warning on an account-switch path, one unsigned update-policy manifest, and one plaintext local cache of health data. Severity was calculated with CVSS v4.0 and OWASP Risk Rating. The actionable findings resolve to two Medium findings and five Low findings under the final report handling. The eighth item is an already-disclosed authentication asymmetry on a recovery endpoint; if scored as a standalone stolen-token risk, it reaches CVSS 6.7 Medium and OWASP Critical, but it is treated here as a documented limitation rather than as a newly discovered defect.
Every actionable finding in this report was checked against the actual source code available at the pinned commit. Where a finding makes a mechanical claim that can be exercised locally (for example, a parser bypass, decompression behavior, or a missing client-side state transition), the relevant mechanism was corroborated with executable proof-of-concept code or direct local execution rather than inferred solely from static reading. Claims that depend on server-side behavior not represented in the public repository are explicitly limited to what can be established from the client code and public project documentation.
2. Scope and Methodology
This report covers the source code publicly available in LifetimeLabsDev/PrivacyNotes.app at commit 3d98da9 (v0.518). The review was independent and unsolicited. No private repository access, production credentials, vendor-provided environment, or non-public implementation detail was used or assumed.
In scope
- Web client source (
application/packages/web), including import/export, sync, rendering/sanitization, settings storage, authentication, and custody-related client flows. - Shared cryptography code and its local known-answer tests.
- Tauri desktop/mobile shell logic to the extent that it is implemented in the public TypeScript/source tree.
- Public project documentation where it clarifies intended security properties, stated trade-offs, or documented limitations.
Not available / not tested
- Server-side infrastructure not present in the public repository, including Supabase RLS policies, server-side edge-function implementations, storage bucket policies, billing webhooks, and deployment configuration.
- Compiled native Tauri binaries. Conclusions about desktop/mobile behavior are limited to the reviewed source code.
- Production infrastructure. No live PrivacyNotes.app service was probed, scanned, load-tested, or destructively tested.
Methodology
The assessment followed a source-first security review process:
- Repository pinning and scope definition. The target repository and commit were fixed before review so all findings refer to a stable code snapshot.
- Architecture and attack-surface mapping. Security-relevant components, trust boundaries, local storage surfaces, import/export paths, authentication flows, sync behavior, rendering paths, and update mechanisms were identified before individual findings were accepted.
- Threat-oriented source review. The code was reviewed by workflow, not only by isolated files. Candidate findings were traced through the surrounding control flow to determine whether the relevant path was reachable and whether another check mitigated the issue later.
- Local corroboration where applicable. When a finding depended on a concrete mechanism that could be exercised locally, such as a parser bypass, decompression behavior, or cryptographic integrity property, it was tested against the public code and dependency behavior in an isolated local environment.
- False-positive reduction. Candidate issues were rejected or downgraded when they depended on unrealistic preconditions, contradicted the documented design, required private/server-side assumptions not present in the repository, or demonstrated a mechanism without an in-scope security impact.
- Severity calculation. Each issue was scored with CVSS v4.0 and OWASP Risk Rating. CVSS vectors are provided for reproducibility. OWASP likelihood and impact were calculated from the methodology's factor averages. Because this was an unsolicited open-source review without access to internal vendor context, OWASP business impact is presented as a public-information proxy, not as an internal vendor risk register.
- Remediation prioritization. Recommendations were ordered by expected security value and implementation cost, with Medium-severity issues prioritized first.
3. Findings Summary
Severity was calculated with two models:
- CVSS v4.0, using the official vector structure and MacroVector/interpolation scoring model.
- OWASP Risk Rating, using likelihood, technical impact, and business-impact factor averages. The business-impact column is a public-information proxy because no internal vendor business context was available.
The final report handling prioritizes the risk model that best captures the supported impact. For local privacy exposure (CA-C-002), OWASP business/privacy impact is more representative than pure technical impact. For the documented custody limitation (F3-013), the item is separated from undisclosed defects even though its conditional stolen-token risk scores higher.
| # | ID | Finding | Final handling | CVSS v4.0 | OWASP technical | OWASP BizRisk proxy | Class |
|---|---|---|---|---|---|---|---|
| 1 | NEW-A7-001 / F3-040 | Backup restore overwrites live notes via an unclamped future timestamp | Medium | 1.2 Low | Medium | Low | Integrity |
| 2 | F3-037 | Declared-size lie on ZIP import entries defeats the size guard; full payload inflates before any check fires | Low | 2.0 Low | Low | Low | Local DoS |
| 3 | F3-036 | CSS-comment / escape sequence bypasses the editor's inline-style denylist | Low | 2.1 Low | Low | Low | Redress / CSS |
| 4 | A3-NEW-2 | Custodial flag committed optimistically before server confirmation, no rollback on failure | Low | 1.1 Low | Low | Low | Integrity |
| 5 | A4-R1-01 | QR / domain-move sign-in bypasses the data-loss confirmation used by other account-switch paths | Low | 1.2 Low | Low | Low | Integrity |
| 6 | A10-R1-02 | Desktop release-floor manifest fetched without an application-layer signature | Low | 1.7 Low | Low | Low | Availability |
| 7 | CA-C-002 | Health data (medications) cached in plaintext in localStorage; PIN-wrap metadata/ciphertext can also persist there on trusted devices | Medium | 5.4 Medium | Low | Medium | Privacy |
| 8 | F3-013 | get-custodial-phrase authenticated by bearer JWT only, unlike signed store/delete | Acknowledged / documented limitation | 6.7 Medium | Critical | Critical | Disclosed limitation |
4. Detailed Findings
4.1 NEW-A7-001 / F3-040 — Backup restore overwrites live notes via an unclamped future timestamp
Severity: Medium
Severity calculation - CVSS v4.0: 1.2 Low — CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:A/VC:N/VI:L/VA:N/SC:N/SI:N/SA:N/E:P; MacroVector 112211. - OWASP Risk Rating: likelihood 5.375 (Medium), technical impact 3.25 (Medium), technical severity Medium; business-impact proxy 1.00 (Low), business severity Low. - Final handling: Medium. Final handling remains Medium because OWASP technical risk captures the integrity impact to existing note content. CVSS is Low because the attack requires active import and a prerequisite such as knowledge of live note IDs or possession of a relevant backup.
Both the client-side import path and the client-side remote-sync write path resolve conflicts with an unbounded "newer wins" rule: whichever copy carries the later updatedAt timestamp is kept, with no upper bound relative to the current time.
Evidence
// application/packages/web/src/import/apply.ts
const incomingAt = n.trashed ? '' : n.updatedAt || '';
if (!(incomingAt > mine.updatedAt)) { unchanged++; return []; }
updated++; The remote conditional update path in sync.ts applies the same unclamped timestamp as the conflict boundary, gating the write on .lte('updated_at', note.updatedAt). This observation is limited to the public client code that builds the update; server-side enforcement behavior was not independently assessed.
Impact A backup manifest crafted with a note id belonging to an existing local note and an updatedAt value set in the future will overwrite that note's current content on restore, regardless of when it was actually last edited. This requires the attacker to already know the target note's id (i.e. to already possess a backup of the victim, or a copy of the manifest), which meaningfully limits real-world reach; it does not expose confidentiality, since the attacker still cannot read the victim's plaintext. The impact is integrity-only: silent, unwanted overwriting of a note the victim believed was safe.
Recommendation Clamp updatedAt on import to the current time (reject or cap future values) before applying the newer-wins comparison, and apply the same policy consistently in the client sync/update path.
4.2 F3-037 — ZIP declared-size lie defeats the import size guard
Severity: Low
Severity calculation - CVSS v4.0: 2.0 Low — CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:A/VC:N/VI:N/VA:L/SC:N/SI:N/SA:N/E:P; MacroVector 102211. - OWASP Risk Rating: likelihood 5.50 (Medium), technical impact 0.50 (Low), technical severity Low; business-impact proxy 0.50 (Low), business severity Low. - Final handling: Low. Final handling remains Low because the impact is local UI-thread denial of service during import, without persistent service outage, data loss, or confidentiality impact.
The import pipeline's per-entry size guard (refuseOverLimit, in zipEntry.ts) checks only the size a ZIP entry declares about itself (entry._data.uncompressedSize) before beginning decompression. It does not track the actual number of bytes produced as decompression proceeds.
Evidence (proof of concept) A ZIP was constructed with jszip containing a highly compressible 1 MiB text entry (compressing to roughly 1 KB). The declared uncompressedSize field was then patched, in both the local file header and the central directory, to a small value the guard would accept. Reading the entry with the project's own zipEntryText() helper does not return the small string it expects; JSZip inflates the real, full-size payload internally before it detects any inconsistency, and only then rejects the read with an internal "uncompressed data size mismatch" error — a consistency check inside JSZip's own decompression worker, which fires on the stream's end event, after decompression has already completed:
// JSZip lib/compressedObject.js
worker.on("end", function () {
if (this.streamInfo["data_length"] !== that.uncompressedSize) {
throw new Error("Bug : uncompressed data size mismatch");
}
}); In other words, the size the application declares to the user ("this import can read at once") is defeated by a false declared size, and the actual denial-of-service cost — inflating attacker-controlled data on the UI thread, which the code's own comments identify as the concern ("such an entry freezes the app before any preview appears") — is paid in full before anything stops it. The eventual rejection is an accident of JSZip's own internal bookkeeping, not a limit PrivacyNotes designed.
By contrast, the same file's gunzipText() function (used for the double-compressed UpNote import path) implements a correct, progressive counter that checks the running total during decompression and aborts mid-stream once the limit is exceeded — demonstrating the codebase already knows the correct pattern; it is simply not applied to the ZIP entry path.
Impact A crafted import file with a small declared size and a large real payload can freeze the importing tab's UI thread for the duration of decompression. Attachments/binary entries are explicitly unbounded by design (per the file's own comments) and out of scope for this specific finding; this concerns the text-entry path only.
Recommendation Bound ZIP text-entry inflation with a progressive byte counter checked during decompression — the same pattern already implemented in gunzipText() — instead of relying on the declared, attacker-controlled size read from the archive's own metadata.
4.3 F3-036 — CSS-comment / escape bypass of the editor's inline-style denylist
Severity: Low
Severity calculation - CVSS v4.0: 2.1 Low — CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:P/VC:N/VI:L/VA:N/SC:N/SI:N/SA:N/E:P; MacroVector 102211. - OWASP Risk Rating: likelihood 5.875 (Medium), technical impact 2.50 (Low), technical severity Low; business-impact proxy 0.50 (Low), business severity Low. - Final handling: Low. Final handling remains Low because the bypass enables UI redress/clickjacking-style manipulation, while CSP and the demonstrated flow do not show script execution, plaintext exfiltration, or durable data modification.
The editor's HTML sanitizer (sanitizeEditorHtml, in markdownRender.ts) filters inline style declarations by checking the property name against an anchored denylist regex (UNSAFE_CSS_PROPS), after splitting on the first colon:
const prop = decl.split(':')[0]?.trim() ?? '';
return prop !== '' && !UNSAFE_CSS_PROPS.test(prop) && !/url\s*\(/i.test(decl); Because the regex is anchored to the start and end of the extracted property token, a declaration such as /**/position:fixed produces the token "/**/position", which does not match position and is therefore allowed through unfiltered.
Evidence (proof of concept) Verified directly, not assumed: a browser-backed proof of concept confirmed that declarations such as <div style="/**/position:fixed"> and escaped variants survive the sanitizer and are interpreted as position: fixed after CSS parsing. CSS comments and escapes are normalized by the browser parser, so the bypass is not only a string-level artifact of the sanitizer.
Impact A crafted note (or a burn-note shared with a stranger) can apply position: fixed and related positioning properties despite the denylist, enabling a clickjacking/UI-redress overlay on the same origin. The project's CSP (script-src 'self') prevents this from being treated as script execution; the demonstrated impact is limited to visual/interaction redress.
Recommendation Replace the property-name string filter with an allowlist enforced by parsing the declaration through a real CSS parser (or the browser's own CSSStyleDeclaration), rather than pattern-matching the raw text of the style attribute.
4.4 A3-NEW-2 — Custodial flag committed before server confirmation, no rollback on failure
Severity: Low
Severity calculation - CVSS v4.0: 1.1 Low — CVSS:4.0/AV:A/AC:H/AT:P/PR:N/UI:A/VC:N/VI:L/VA:L/SC:N/SI:N/SA:N/E:P; MacroVector 112211. - OWASP Risk Rating: likelihood 4.375 (Medium), technical impact 2.25 (Low), technical severity Low; business-impact proxy 1.50 (Low), business severity Low. - Final handling: Low. Final handling remains Low because the bug depends on a narrow failure window during custody adoption and primarily creates account-recovery confusion rather than direct compromise.
During custodial onboarding, the client sets local authentication state — including isCustodial: true — before the server call that actually stores the recovery phrase (store-custodial-phrase) is confirmed to have succeeded.
Evidence
// auth.tsx — completeCustodyChoice
const authenticated = await authenticateWithPhrase(phrase, 'oauth', isCustodial, ...);
// ...
await adoptCustodyServer({ ... }); // store-custodial-phrase — may still throw here If adoptCustodyServer throws, the surrounding catch block returns { ok: false, error } to the caller, but does not revert the isCustodial flag already committed by authenticateWithPhrase. The lazy healing path does not correct this either: authCustody.ts's reconciliation effect explicitly treats a null server-side custodial claim as nothing to reconcile (if (claim === null || claim === authedCustodial) return;), leaving the optimistic true flag in place indefinitely.
Impact A user whose store-custodial-phrase call fails (network error, transient server failure) can be left believing their recovery phrase is safely stored server-side for one-click future sign-in, when it is not. If they never separately wrote the phrase down — a reasonable assumption for a custodial user, since the entire point of the custodial option is not needing to — they are one lost session away from permanent account loss.
Recommendation Order the operation store-before-flag: only set isCustodial: true locally after adoptCustodyServer confirms success, and add an explicit rollback in the failure path rather than relying on lazy reconciliation that currently no-ops on a null claim.
4.5 A4-R1-01 — QR / domain-move handoff bypasses the data-loss confirmation used elsewhere
Severity: Low
Severity calculation - CVSS v4.0: 1.2 Low — CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:A/VC:N/VI:N/VA:L/SC:N/SI:N/SA:N/E:P; MacroVector 112211. - OWASP Risk Rating: likelihood 5.00 (Medium), technical impact 1.75 (Low), technical severity Low; business-impact proxy 1.50 (Low), business severity Low. - Final handling: Low. Final handling remains Low because the issue can cause local data loss in a narrow handoff/account-switch flow but requires active user participation and does not affect note-content confidentiality.
Other account-switch paths in the client (standard onboarding, OAuth sign-in) explicitly check switchWouldWipe() and, if true, require the user to confirm via askAccountSwitch() before proceeding — an explicit "this will erase local data" gate. The QR sign-in / domain-move handoff path does not call either function.
Evidence App.tsx's handleConfirmPendingPhrase, wired to the QrSignInPrompt confirmation dialog, calls signInWithPhrase(pendingSignIn.phrase, ...) directly. A code comment in the same file acknowledges the gap for one edge case ("Switching accounts from a QR is too risky without a proper 'switch account' flow that also wipes local data first") but only guards the already-authenticated case by silently dropping the fragment; the unauthenticated / fresh-device case proceeds without the warning.
Impact A user who scans a sign-in QR or completes a domain-move handoff on a device that already holds unrelated local data (a different account's notes, unsynced drafts) can have that data wiped without seeing the specific loss warning shown on every other account-switch path in the app.
Recommendation Route the QR / domain-move confirmation through the same switchWouldWipe / askAccountSwitch gate, or add an equivalent explicit warning to QrSignInPrompt itself.
4.6 A10-R1-02 — Desktop release-floor manifest fetched without an application-layer signature
Severity: Low
Severity calculation - CVSS v4.0: 1.7 Low — CVSS:4.0/AV:N/AC:H/AT:P/PR:N/UI:N/VC:N/VI:N/VA:L/SC:N/SI:N/SA:N/E:U; MacroVector 012221. - OWASP Risk Rating: likelihood 5.375 (Medium), technical impact 1.50 (Low), technical severity Low; business-impact proxy 1.50 (Low), business severity Low. - Final handling: Low. Final handling remains Low because exploitation requires influence over the HTTPS policy response or release-floor host, and the demonstrated impact is reversible availability degradation.
The desktop updater's Linux release-floor check (fetchLinuxMinVersion, in DesktopUpdater.tsx) fetches a JSON policy document over HTTPS and reads minVersion directly from the response body, with no application-layer signature, hash pin, or integrity check beyond transport security.
Evidence
const res = await fetch(`${LINUX_POLICY_URL}?t=${Date.now()}`, { cache: 'no-store' });
const p = (await res.json()) as { minVersion?: string };
return typeof p?.minVersion === 'string' ? p.minVersion : null; This is inconsistent with the desktop binary itself, which the project documents as minisign-pinned.
Impact A party able to influence this HTTPS response (for example, through compromise of the release-floor host, trusted TLS interception on the device, or another content-control failure) can pause sync for affected clients by serving an inflated minVersion. Impact is limited to availability and is reversible once the correct policy is restored; it does not affect confidentiality or integrity of note content.
Recommendation Sign release-floor manifests and verify the signature client-side, or serve the floor value through the same signed channel the binary update itself uses.
4.7 CA-C-002 — Health data cached in plaintext in localStorage
Severity: Medium
Severity calculation - CVSS v4.0: 5.4 Medium — CVSS:4.0/AV:L/AC:L/AT:N/PR:L/UI:N/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N/E:P; MacroVector 101210. - OWASP Risk Rating: likelihood 5.375 (Medium), technical impact 1.75 (Low), technical severity Low; business-impact proxy 3.50 (Medium), business severity Medium. - Final handling: Medium. Final handling remains Medium because CVSS captures high local confidentiality impact and OWASP business impact is elevated by the health-related nature of the plaintext medication data.
The local settings cache writes user settings to localStorage without applying the note-at-rest sealing mechanism. The sensitive field that remains exposed across the settings cache is medications (drug name, dose, schedule, history). On trusted devices, PIN-wrap metadata/ciphertext (pinWrapSalt, pinWrapIV, pinWrapCiphertext, pinWrapIterations) can also be persisted in the same unsealed settings object; on untrusted devices, the PIN-wrap fields are withheld from localStorage and kept in session storage, but medications still remains in the local settings cache.
Evidence
// userSettings.ts — writeLocal()
localStorage.setItem(localKey(), JSON.stringify(stored)); // settings cache; no localSeal applied
// trusted-device path
if (trusted && !wrapWithheld) {
// stored may include pinWrap* fields as well as medications
} By contrast, note content is explicitly sealed at rest via localSeal.ts (used in db.ts and notesRepo.ts) before it ever reaches local storage; userSettings.ts does not import or use localSeal anywhere in the file.
Impact On a lost or stolen device, an attacker with local storage access — a realistic threat for a privacy-focused product, and one the product's notes already defend against via sealing — can read the user's medication list in plaintext while notes remain protected. On trusted devices, PIN-wrap metadata/ciphertext can also persist in the same unsealed local cache. Medication data may be treated as sensitive or special-category data under privacy frameworks such as GDPR Art. 9, making the exposure relevant beyond purely technical impact. This does not demonstrate a server-side E2EE break: the backend still receives encrypted note content. The issue is a local-storage-at-rest inconsistency.
Recommendation Encrypt the settings cache at rest using the same at-rest key/mechanism as notes (localSeal), or exclude health-related fields from the local cache entirely and re-fetch them decrypted only into memory when needed.
4.8 F3-013 — get-custodial-phrase authenticated by bearer token only
Severity: Acknowledged — disclosed limitation
Severity calculation - CVSS v4.0: 6.7 Medium — CVSS:4.0/AV:N/AC:L/AT:P/PR:L/UI:N/VC:H/VI:H/VA:N/SC:N/SI:N/SA:N/E:P; MacroVector 110210. - OWASP Risk Rating: likelihood 7.375 (High), technical impact 6.00 (High), technical severity Critical; business-impact proxy 6.00 (High), business severity Critical. - Final handling: Acknowledged / documented limitation. This item remains outside the normal undisclosed-defect list because the project publicly documents the bearer-only trade-off. If scored as a standalone risk under stolen-token conditions, OWASP risk is Critical because phrase retrieval can compromise custodial account confidentiality and integrity; CVSS remains Medium due to PR:L and AT:P.
The public client retrieval flow for get-custodial-phrase uses a valid session JWT alone; the store and delete operations on the same resource additionally include an Ed25519 signature over a challenge.
Evidence
// devices.ts — store/delete both sign a challenge
body: { phrase, signature: bytesToHex(signature) } // store-custodial-phrase
body: { signature: bytesToHex(signature) } // delete-custodial-phrase
// authOAuth.ts — get-custodial-phrase
headers: { Authorization: `Bearer ${accessToken}` } // no signature field This asymmetry is also disclosed in the project's own THREAT_MODEL.md and SECURITY.md, which state that get-custodial-phrase requires only a valid JWT and no signature challenge, and explain the constraint: at the point this call is made, the user may not yet hold a signing key, since the phrase itself is what derives it.
Impact Under the documented design, a stolen session token can retrieve a custodial user's recovery phrase for the token's remaining lifetime. This is scoped to custodial-mode users only (self-custody users have no server-side copy to retrieve). The authentication asymmetry is publicly documented by the project, so this report treats it as a disclosed design limitation rather than presenting it as a newly discovered server-side defect.
Recommendation Because the limitation is already publicly documented, this report does not treat it as an undisclosed defect. If the design later permits a signing key to be established before phrase retrieval, adding a signature challenge would reduce the residual risk.
5. Remediation Priorities
Ordered by cost/impact; priorities 1–2 address the two Medium-severity findings first.
| Priority | Action | Finding(s) |
|---|---|---|
| 1 | Validate and clamp updated_at on import; reject timestamps in the future relative to server time | NEW-A7-001 / F3-040 |
| 2 | Encrypt the local settings cache at rest (reuse the notes' localSeal), or exclude health fields from it entirely | CA-C-002 |
| 3 | Reorder custody adoption to store-before-flag, and roll back isCustodial on a failed store-custodial-phrase call | A3-NEW-2 |
| 4 | Sign release-floor manifests, or serve minVersion over the same channel the desktop binary is pinned through | A10-R1-02 |
| 5 | Replace the property-name denylist in sanitizeEditorHtml with a real CSS parser / allowlist | F3-036 |
| 6 | Bound ZIP entry inflation with a progressive byte counter during decompression, mirroring gunzipText's pattern, instead of trusting the declared size upfront | F3-037 |
| 7 | Add an explicit data-loss notice to the QR / domain-move confirmation prompt | A4-R1-01 |
| 8 | Consider a signature challenge for get-custodial-phrase if a viable signing-key bootstrap becomes available; documented limitation until then | F3-013 |
6. Limitations
- This was an independent, unsolicited open-source review. It relied exclusively on material publicly available in the repository at the pinned commit and on local execution of that code.
- Server-side infrastructure not present in the public repository (Supabase RLS policies, server-side edge-function implementations, Paddle billing webhooks, storage bucket policies, and related deployment configuration) was not assessable. No conclusion is drawn about the security of those components.
- Compiled native Tauri binaries (macOS/Windows/Linux/iOS/Android) were not built or tested; conclusions about the desktop/mobile shell are limited to the source code reviewed.
- No production credentials, privileged access, project-supplied test environment, or private implementation details were available or assumed.
- No penetration testing, load testing, infrastructure scanning, or destructive testing was performed against any live PrivacyNotes.app service.
7. Conclusion
No client-side cryptographic break was identified in the reviewed source code. The available integrity tests for the reviewed AEAD paths passed under direct local execution. The report identifies seven actionable defects and one documented limitation; the actionable issues sit in integrity, availability, or local-metadata handling rather than in demonstrated compromise of note-content confidentiality. Under the integrated CVSS v4.0 and OWASP scoring, the actionable undisclosed findings remain Low/Medium only. The documented get-custodial-phrase limitation scores higher if evaluated as a standalone stolen-token risk, but it is not presented as a newly discovered defect. The most actionable items — the unclamped restore timestamp and the plaintext health-data cache — are prioritized in Section 5.
The principal unresolved area is the server-side surface that is not present in the reviewed public repository. Its security cannot be inferred from the client alone. A separate assessment would require the relevant server-side source and configuration; this report makes no claim about those components.
8. References
- OWASP Risk Rating Methodology: https://owasp.org/www-community/OWASP_Risk_Rating_Methodology
- CVSS v4.0 Specification: https://www.first.org/cvss/v4.0/specification-document
- FIRST CVSS v4 Calculator reference implementation: https://github.com/FIRSTdotorg/cvss-v4-calculator
Eduardo Camarillo Security Researcher Contact: [email protected] Web: noir0x63.org GitHub: Noir0x63