Compare commits
2 commits
7409077355
...
015a99a7fe
| Author | SHA1 | Date | |
|---|---|---|---|
| 015a99a7fe | |||
|
|
e322145ffb |
6 changed files with 737 additions and 990 deletions
|
|
@ -11,14 +11,15 @@ the documentation reconciliation. They are historical context, not current specs
|
||||||
|--------|-------|
|
|--------|-------|
|
||||||
| [tech-stack-decision.md](./tech-stack-decision.md) | Master stack choice: Rust/Axum backend, React frontend, MongoDB, JWT |
|
| [tech-stack-decision.md](./tech-stack-decision.md) | Master stack choice: Rust/Axum backend, React frontend, MongoDB, JWT |
|
||||||
| [mongodb-schema-decision.md](./mongodb-schema-decision.md) | Document model + at-rest encryption approach |
|
| [mongodb-schema-decision.md](./mongodb-schema-decision.md) | Document model + at-rest encryption approach |
|
||||||
| [jwt-authentication-decision.md](./jwt-authentication-decision.md) | JWT access/refresh tokens, recovery phrases |
|
| [jwt-authentication-decision.md](./jwt-authentication-decision.md) | JWT access/refresh tokens, `token_version` revocation, refresh rotation — *reconciled with code 2026-07-18* |
|
||||||
| [frontend-decision-summary.md](./frontend-decision-summary.md) | React (web) + React Native (mobile, future) split |
|
| [frontend-decision-summary.md](./frontend-decision-summary.md) | React (web) + React Native (mobile, future) split |
|
||||||
| [state-management-decision.md](./state-management-decision.md) | Client state — *superseded*: decision was Redux Toolkit, **actual code uses Zustand** |
|
| [state-management-decision.md](./state-management-decision.md) | Client state — *superseded*: decision was Redux Toolkit, **actual code uses Zustand** |
|
||||||
| [monorepo-structure.md](./monorepo-structure.md) | Repository layout (`backend/`, `web/`, `mobile/`, `docs/`) |
|
| [monorepo-structure.md](./monorepo-structure.md) | Repository layout (`backend/`, `web/`, `mobile/`, `docs/`) |
|
||||||
| [backend-deployment-constraints.md](./backend-deployment-constraints.md) | Deployment requirements (Solaria, Docker) |
|
| [backend-deployment-constraints.md](./backend-deployment-constraints.md) | Deployment requirements (Solaria, Docker) |
|
||||||
| [mobile-health-frameworks-data.md](./mobile-health-frameworks-data.md) | HealthKit / Health Connect data-type reference (for future mobile work) |
|
| [mobile-health-frameworks-data.md](./mobile-health-frameworks-data.md) | HealthKit / Health Connect data-type reference (for future mobile work) |
|
||||||
| [android-health-connect-data-types.md](./android-health-connect-data-types.md) | Android Health Connect data types |
|
| [android-health-connect-data-types.md](./android-health-connect-data-types.md) | Android Health Connect data types |
|
||||||
| [zero-knowledge-encryption.md](./zero-knowledge-encryption.md) | Client-side zero-knowledge encryption (AES-256-GCM, double-PBKDF2, Phase 1) |
|
| [zero-knowledge-encryption.md](./zero-knowledge-encryption.md) | Client-side zero-knowledge encryption (AES-256-GCM, double-PBKDF2, wrapped-DEK recovery; Phase 1 + Phase 2 implemented) |
|
||||||
|
| [multi-person-sharing.md](./multi-person-sharing.md) | Per-profile DEK + X25519 envelope for family/caregiver sharing — *Decided, not yet implemented* (issue #3) |
|
||||||
|
|
||||||
> **Note**: Where a decision diverges from the implemented code (e.g. state
|
> **Note**: Where a decision diverges from the implemented code (e.g. state
|
||||||
> management), the code is the source of truth and the ADR is kept only as
|
> management), the code is the source of truth and the ADR is kept only as
|
||||||
|
|
|
||||||
|
|
@ -1,174 +1,185 @@
|
||||||
# JWT Authentication Decision Summary
|
# ADR: JWT Authentication
|
||||||
|
|
||||||
**Date**: 2026-02-14
|
**Status**: Implemented (current code is the source of truth)
|
||||||
**Decision**: **JWT with Refresh Tokens + Recovery Phrases**
|
**Date**: 2026-02-14 (original decision), 2026-07-18 (reconciled with code)
|
||||||
|
|
||||||
---
|
> This ADR was originally written during Phase 1 research and described several
|
||||||
|
> things that were never built (bcrypt, Redis, family-role permission claims in
|
||||||
|
> the JWT, the specific recovery-phrase flow). It has been rewritten to match
|
||||||
|
> the implementation. The original reasoning is preserved in the "History"
|
||||||
|
> section at the end. Where code and the old text disagree, **the code wins**.
|
||||||
|
|
||||||
## Authentication Strategy
|
## Context
|
||||||
|
|
||||||
### Primary: JWT (JSON Web Tokens)
|
Normogen authenticates accounts with email + password and authorizes API
|
||||||
|
requests with stateless JWTs. This sits *underneath* the zero-knowledge
|
||||||
|
encryption layer (see [`zero-knowledge-decryption.md`](./zero-knowledge-encryption.md)
|
||||||
|
and [`encryption.md`](../product/encryption.md)): authentication proves who you
|
||||||
|
are and lets you fetch your wrapped DEK; the client-side ZK layer then unlocks
|
||||||
|
your data. The auth system never sees plaintext health data.
|
||||||
|
|
||||||
**Why JWT?**
|
## Decision
|
||||||
- Stateless design scales to 1000+ concurrent connections
|
|
||||||
- Works perfectly with mobile apps (AsyncStorage)
|
|
||||||
- No server-side session storage needed
|
|
||||||
- Easy to scale Axum horizontally
|
|
||||||
|
|
||||||
### Token Types
|
### Token model
|
||||||
|
|
||||||
**Access Token** (15 minutes)
|
Two JWTs, both HS256-signed with a shared secret from config:
|
||||||
- Used for API requests
|
|
||||||
- Short-lived for security
|
|
||||||
- Contains: user_id, email, family_id, permissions
|
|
||||||
|
|
||||||
**Refresh Token** (30 days)
|
| Token | Lifetime | Claims | Purpose |
|
||||||
- Used to get new access tokens
|
|---|---|---|---|
|
||||||
- Long-lived for convenience
|
| Access | configurable (`JwtConfig.access_token_expiry_minutes`, default **15 min**) | `{sub, exp, iat, user_id, email, token_version}` | Sent on every API request |
|
||||||
- Stored in MongoDB for revocation
|
| Refresh | configurable (`JwtConfig.refresh_token_expiry_days`, default **30 days**) | `{sub, exp, iat, jti, user_id, token_version}` | Exchange for a new access token |
|
||||||
- Rotated on every refresh
|
|
||||||
|
|
||||||
---
|
> Reference: `backend/src/auth/jwt.rs` (`Claims`, `RefreshClaims`, `JwtService`).
|
||||||
|
|
||||||
## Token Revocation Strategies
|
The lifetimes are **config-driven**, not hardcoded — the original ADR's "15 min
|
||||||
|
/ 30 days" are the defaults, not constants.
|
||||||
|
|
||||||
### 1. Refresh Token Blacklist (Recommended) ⭐
|
### Claims (actual, as implemented)
|
||||||
- Store refresh tokens in MongoDB
|
|
||||||
- Mark as revoked on logout
|
|
||||||
- Check on every refresh
|
|
||||||
|
|
||||||
### 2. Token Versioning
|
The earlier ADR listed richer claims (`family_id`, `permissions`, `token_type`).
|
||||||
- Include version in JWT claims
|
**These are not in the shipped access token.** The real `Claims` struct is:
|
||||||
- Increment on password change
|
|
||||||
- Invalidate all tokens when version changes
|
|
||||||
|
|
||||||
### 3. Access Token Blacklist (Optional)
|
```rust
|
||||||
- Store revoked access tokens in Redis
|
pub struct Claims { // access token
|
||||||
- For immediate revocation
|
pub sub: String, // = user_id (ObjectId hex)
|
||||||
- Auto-expires with TTL
|
pub exp: usize,
|
||||||
|
pub iat: usize,
|
||||||
|
pub user_id: String,
|
||||||
|
pub email: String,
|
||||||
|
pub token_version: i32,
|
||||||
|
}
|
||||||
|
|
||||||
---
|
pub struct RefreshClaims { // refresh token
|
||||||
|
pub sub: String,
|
||||||
## Refresh Token Pattern
|
pub exp: usize,
|
||||||
|
pub iat: usize,
|
||||||
### Token Rotation (Security Best Practice) ⭐
|
pub jti: String, // unique per token (see below)
|
||||||
|
pub user_id: String,
|
||||||
**Flow**:
|
pub token_version: i32,
|
||||||
1. Client sends refresh_token
|
|
||||||
2. Server verifies refresh_token (not revoked, not expired)
|
|
||||||
3. Server generates new access_token
|
|
||||||
4. Server generates new refresh_token
|
|
||||||
5. Server revokes old refresh_token
|
|
||||||
6. Server returns new tokens
|
|
||||||
|
|
||||||
**Why?** Prevents reuse of stolen refresh tokens
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Zero-Knowledge Password Recovery
|
|
||||||
|
|
||||||
### Recovery Phrases (from encryption.md)
|
|
||||||
|
|
||||||
**Registration**:
|
|
||||||
1. Client generates recovery phrase (random 32 bytes)
|
|
||||||
2. Client encrypts recovery phrase with password
|
|
||||||
3. Client sends: email, password hash, encrypted recovery phrase
|
|
||||||
4. Server stores: email, password hash, encrypted recovery phrase
|
|
||||||
|
|
||||||
**Password Recovery**:
|
|
||||||
1. User requests recovery (enters email)
|
|
||||||
2. Server returns: encrypted recovery phrase
|
|
||||||
3. Client decrypts with recovery key (user enters manually)
|
|
||||||
4. User enters new password
|
|
||||||
5. Client re-encrypts recovery phrase with new password
|
|
||||||
6. Client sends: new password hash, re-encrypted recovery phrase
|
|
||||||
7. Server updates: password hash, encrypted recovery phrase, token_version + 1
|
|
||||||
8. All existing tokens invalidated (version mismatch)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Family Member Access Control
|
|
||||||
|
|
||||||
### Permissions in JWT
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// JWT permissions based on family role
|
|
||||||
{
|
|
||||||
"parent": [
|
|
||||||
"read:own_data",
|
|
||||||
"write:own_data",
|
|
||||||
"read:family_data",
|
|
||||||
"write:family_data",
|
|
||||||
"manage:family_members",
|
|
||||||
"delete:data"
|
|
||||||
],
|
|
||||||
"child": [
|
|
||||||
"read:own_data",
|
|
||||||
"write:own_data"
|
|
||||||
],
|
|
||||||
"elderly": [
|
|
||||||
"read:own_data",
|
|
||||||
"write:own_data",
|
|
||||||
"read:family_data"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Permission Middleware
|
Note what is **absent** and why:
|
||||||
|
|
||||||
- Check permissions on protected routes
|
- **No `family_id` or `permissions` in the token.** Family/permission
|
||||||
- Return 403 Forbidden if insufficient permissions
|
enforcement is not implemented at the auth layer (see issue #3 — multi-person
|
||||||
- Works with JWT claims
|
sharing is an open design problem). Putting unenforced claims in the token
|
||||||
|
would imply protection that doesn't exist.
|
||||||
|
- `backend/src/auth/claims.rs` *does* define an `AccessClaims` struct with
|
||||||
|
`family_id`, `permissions`, `token_type`, `jti`. **It is dead code** — not
|
||||||
|
imported or used anywhere in the source (only in stale compiler scratch
|
||||||
|
files under `target/`). It should be removed or wired up; tracked by issue #4.
|
||||||
|
- **No `token_type` discriminator.** Access and refresh claims are different
|
||||||
|
structs, so a token can only decode as one or the other — the field is
|
||||||
|
redundant and was dropped.
|
||||||
|
|
||||||
---
|
### Refresh tokens: persistence, rotation, reuse detection
|
||||||
|
|
||||||
## Technology Stack
|
- Refresh tokens are **stored in MongoDB** (`refresh_tokens` collection) — not
|
||||||
|
Redis (the original ADR listed Redis as an option; it is not used).
|
||||||
|
- The `jti` claim makes every refresh token unique. Without it, two refresh
|
||||||
|
tokens issued in the same second for the same user would be byte-identical,
|
||||||
|
which breaks rotation and reuse detection. (Covered by the
|
||||||
|
`refresh_tokens_are_unique_even_in_same_second` test in `jwt.rs`.)
|
||||||
|
- On refresh: validate signature + expiry, confirm the token exists and is not
|
||||||
|
revoked in the repository, mint a new access + refresh pair, revoke the old
|
||||||
|
refresh token (rotation).
|
||||||
|
|
||||||
### Backend (Axum)
|
### Token revocation: versioning + session management
|
||||||
- jsonwebtoken 9.x (JWT crate)
|
|
||||||
- bcrypt 0.15 (password hashing)
|
|
||||||
- mongodb 3.0 (refresh token storage)
|
|
||||||
- redis (optional, for access token blacklist)
|
|
||||||
|
|
||||||
### Client (React Native + React)
|
Two mechanisms, both implemented:
|
||||||
- AsyncStorage (token storage)
|
|
||||||
- axios (API client with JWT interceptor)
|
|
||||||
- PBKDF2 (password derivation)
|
|
||||||
- AES-256-GCM (data encryption)
|
|
||||||
|
|
||||||
---
|
1. **`token_version` (global kill-switch).** Stored on the `User` document,
|
||||||
|
incremented on password change and password recovery. The JWT middleware
|
||||||
|
(`middleware/auth.rs`) rejects any access token whose `token_version` is
|
||||||
|
stale against the user's current value. A short-lived `TokenVersionCache`
|
||||||
|
avoids hitting Mongo on every request.
|
||||||
|
2. **Session management.** Refresh tokens are tracked as sessions
|
||||||
|
(`GET /api/sessions`, `DELETE /api/sessions/:id`, `DELETE /api/sessions/all`)
|
||||||
|
so a user can list and revoke individual logins.
|
||||||
|
|
||||||
## Implementation Timeline
|
### Password handling
|
||||||
|
|
||||||
- **Week 1**: Basic JWT (login, register, middleware)
|
- **PBKDF2** (not bcrypt — the original ADR mentioned bcrypt), via the `pbkdf2`
|
||||||
- **Week 1-2**: Refresh tokens (storage, rotation)
|
crate with a per-password random salt. (`backend/src/auth/password.rs`)
|
||||||
- **Week 2**: Token revocation (blacklist, versioning)
|
- Under the ZK model, what the server hashes is the **auth secret** — a
|
||||||
- **Week 2-3**: Password recovery (recovery phrases)
|
base64'd PBKDF2 derivation of the user's password produced client-side — not
|
||||||
- **Week 3**: Family access control (permissions)
|
the raw password. The raw password never leaves the browser. See
|
||||||
- **Week 3-4**: Security hardening (rate limiting, HTTPS)
|
[`encryption.md`](../product/encryption.md) §2.
|
||||||
|
|
||||||
**Total**: 3-4 weeks
|
### Password recovery
|
||||||
|
|
||||||
---
|
Recovery is handled by the **zero-knowledge wrapped-DEK** mechanism, *not* by
|
||||||
|
the "client encrypts the recovery phrase with the password" scheme the original
|
||||||
|
ADR described. The client derives a recovery KEK from the recovery phrase,
|
||||||
|
unwraps the DEK from the recovery-wrapped form, and re-wraps it under the new
|
||||||
|
password. See [`zero-knowledge-encryption.md`](./zero-knowledge-encryption.md)
|
||||||
|
§"Phase 2: Wrapped-DEK Recovery". The server bumps `token_version` on recovery,
|
||||||
|
invalidating all prior tokens.
|
||||||
|
|
||||||
## Next Steps
|
### What protects what
|
||||||
|
|
||||||
1. Implement basic JWT service in Axum
|
Auth + the ciphertext store are defended server-side; plaintext health data is
|
||||||
2. Create MongoDB schema for users and refresh tokens
|
never on the server to protect:
|
||||||
3. Implement login/register/refresh/logout handlers
|
|
||||||
4. Create JWT middleware for protected routes
|
|
||||||
5. Implement token revocation (blacklist + versioning)
|
|
||||||
6. Integrate password recovery (from encryption.md)
|
|
||||||
7. Implement family access control (permissions)
|
|
||||||
8. Test entire authentication flow
|
|
||||||
9. Create client-side authentication (React Native + React)
|
|
||||||
|
|
||||||
---
|
| Layer | Mechanism |
|
||||||
|
|---|---|
|
||||||
|
| Password / auth secret | PBKDF2 + per-password salt |
|
||||||
|
| Tokens | HS256 signature, short access TTL, refresh rotation + `jti` reuse detection |
|
||||||
|
| Revocation | `token_version` (global) + session list (per-login) |
|
||||||
|
| Brute force | Account lockout (5 attempts → exponential backoff) + rate limiting |
|
||||||
|
| Forensics | Audit logging (auth attempts, authz checks) |
|
||||||
|
|
||||||
|
> Reference: `backend/src/auth/`, `backend/src/security/`, `backend/src/middleware/`.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
### Positive
|
||||||
|
|
||||||
|
- Stateless access tokens scale horizontally (no DB lookup per request except
|
||||||
|
the cached version check).
|
||||||
|
- `token_version` gives instant global revocation on password change/recovery
|
||||||
|
without maintaining a blocklist of outstanding tokens.
|
||||||
|
- Refresh rotation + `jti` detects token theft: a rotated-away token being
|
||||||
|
presented again signals reuse.
|
||||||
|
|
||||||
|
### Negative / costs
|
||||||
|
|
||||||
|
- `token_version` revocation is **coarse** — it kills *all* of a user's
|
||||||
|
sessions at once. Per-session revocation is only available via the session
|
||||||
|
list (refresh-token deletion).
|
||||||
|
- HS256 with a shared secret means the secret is sensitive infrastructure; a
|
||||||
|
key rotation requires invalidating all tokens (acceptable for the deployment
|
||||||
|
model, but worth noting vs. asymmetric RS/ES keys).
|
||||||
|
- The dead `AccessClaims` struct is a latent trap — someone could wire it up
|
||||||
|
assuming `family_id`/`permissions` are enforced. Should be deleted.
|
||||||
|
|
||||||
|
## Open items
|
||||||
|
|
||||||
|
- **Remove `backend/src/auth/claims.rs`** (dead `AccessClaims`) or wire it up
|
||||||
|
intentionally. Tracked under issue #4.
|
||||||
|
- When issue #3 (multi-person sharing) is decided, revisit whether family /
|
||||||
|
share authorization belongs in the JWT or is enforced per-request against a
|
||||||
|
shares collection.
|
||||||
|
|
||||||
|
## History (original 2026-02-14 decision, superseded where noted)
|
||||||
|
|
||||||
|
The original ADR proposed: JWT with refresh tokens; bcrypt; optional Redis for
|
||||||
|
access-token blacklists; recovery by client-encrypting the recovery phrase with
|
||||||
|
the password; and `family_id`/`permissions` claims with parent/child/elderly
|
||||||
|
role matrices. Of these, only **JWT with refresh tokens + rotation** was built
|
||||||
|
as described. Password hashing uses PBKDF2 (not bcrypt), there is no Redis,
|
||||||
|
recovery uses the wrapped-DEK scheme (not phrase-encryption), and the family/
|
||||||
|
permission claims were never wired into the real `Claims`. This section is kept
|
||||||
|
as a record of the original reasoning; the sections above are authoritative.
|
||||||
|
|
||||||
## References
|
## References
|
||||||
|
|
||||||
- [Comprehensive JWT Research](./2026-02-14-jwt-authentication-research.md)
|
- Code: `backend/src/auth/jwt.rs`, `backend/src/auth/password.rs`,
|
||||||
- [Normogen Encryption Guide](../encryption.md)
|
`backend/src/middleware/auth.rs`, `backend/src/auth/token_version_cache.rs`,
|
||||||
- [JWT RFC 7519](https://tools.ietf.org/html/rfc7519)
|
`backend/src/models/refresh_token.rs`, `backend/src/models/session.rs`
|
||||||
- [Axum JWT Guide](https://docs.rs/axum/latest/axum/)
|
- Related ADRs: [`zero-knowledge-encryption.md`](./zero-knowledge-encryption.md),
|
||||||
- [OWASP JWT Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/JSON_Web_Token_for_Java_Cheat_Sheet.html)
|
[`multi-person-sharing.md`](./multi-person-sharing.md) (draft)
|
||||||
|
- Product doc: [`encryption.md`](../product/encryption.md)
|
||||||
|
- Related issues: [#3](https://gitea.soliverez.com.ar/alvaro/normogen/issues/3),
|
||||||
|
[#4](https://gitea.soliverez.com.ar/alvaro/normogen/issues/4)
|
||||||
|
|
|
||||||
336
docs/adr/multi-person-sharing.md
Normal file
336
docs/adr/multi-person-sharing.md
Normal file
|
|
@ -0,0 +1,336 @@
|
||||||
|
# ADR: Multi-Person Sharing Under Zero-Knowledge
|
||||||
|
|
||||||
|
**Status**: Decided (not yet implemented)
|
||||||
|
**Date**: 2026-07-18
|
||||||
|
**Discussion**: [issue #3](https://gitea.soliverez.com.ar/alvaro/normogen/issues/3)
|
||||||
|
**Related**: [`zero-knowledge-encryption.md`](./zero-knowledge-encryption.md) (the single-user ZK model this extends)
|
||||||
|
|
||||||
|
> Records the design decision and its rationale ahead of implementation,
|
||||||
|
> following the pattern of `zero-knowledge-encryption.md`. The five original
|
||||||
|
> open questions were resolved on 2026-07-18 (see "Open questions" section).
|
||||||
|
> Two deferred items remain (pubkey verification mechanism; admin re-key
|
||||||
|
> quorum) — neither blocks implementation. All crypto primitives named here
|
||||||
|
> are available in the browser's Web Crypto API.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
Normogen's core product vision (see
|
||||||
|
[`docs/product/PERSONA_AND_FAMILY_MANAGEMENT.md`](../product/PERSONA_AND_FAMILY_MANAGEMENT.md))
|
||||||
|
is multi-person health data: a parent manages a child's medications and shares
|
||||||
|
them with a spouse or caregiver. The existing zero-knowledge model
|
||||||
|
(`zero-knowledge-encryption.md`) is single-user: **all of a user's data is
|
||||||
|
encrypted under one DEK that only that user can unwrap.** There is no mechanism
|
||||||
|
for a second user to decrypt any of it. As called out in issue #3, the current
|
||||||
|
`Share` model + `/api/shares` routes store ACL-style permission records but
|
||||||
|
cannot grant decryption access to another account — every medication blob is
|
||||||
|
under the owner's DEK, and the recipient has a different one.
|
||||||
|
|
||||||
|
The product requirements that constrain this design (confirmed in issue #3):
|
||||||
|
|
||||||
|
1. **Parents and spouses have separate logins**, and may belong to *different*
|
||||||
|
families. Divorced parents with their own families must be able to share the
|
||||||
|
profile of their common child **and nothing else**.
|
||||||
|
2. **Recipients always have Normogen accounts.** (One-way document dumps to
|
||||||
|
non-accounts — e.g. a physician via an encrypted PDF — is a separate,
|
||||||
|
simpler feature, out of scope here.)
|
||||||
|
3. **Revocation is a hard requirement.** Cutting off a caregiver must take
|
||||||
|
effect.
|
||||||
|
4. **The server operator is fully untrusted.** Key exchange cannot rely on the
|
||||||
|
server brokering symmetric secrets.
|
||||||
|
|
||||||
|
Additional use cases from issue #3 that the model must accommodate:
|
||||||
|
|
||||||
|
- **Profile graduation.** A child profile eventually becomes an independent
|
||||||
|
user with their own login, who may then revoke the parents' access.
|
||||||
|
- **Pets.** A profile may be an animal (medications, labs, etc.), not a human.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
Implement **per-profile DEKs with an X25519 asymmetric envelope** for sharing,
|
||||||
|
keeping the server a fully blind store. Concretely:
|
||||||
|
|
||||||
|
### 1. Key hierarchy
|
||||||
|
|
||||||
|
```
|
||||||
|
password KEK ──wraps──▶ account DEK ──wraps each──▶ profile DEK ──encrypts──▶ profile data
|
||||||
|
│ (one per profile)
|
||||||
|
└──also wraps──▶ account X25519 private key
|
||||||
|
(account X25519 public key stored plaintext)
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Account DEK**: random AES-256-GCM key, wrapped under the password KEK
|
||||||
|
(unchanged from today). At login the client unwraps it and holds it in
|
||||||
|
memory.
|
||||||
|
- **Profile DEK**: a *new* random AES-256-GCM key **per profile**. All data
|
||||||
|
belonging to a profile (its medications, appointments, etc.) is encrypted
|
||||||
|
under the profile DEK, not the account DEK. Each profile DEK is wrapped under
|
||||||
|
the account DEK and stored on the server, indexed by `profileId`.
|
||||||
|
- **Account X25519 keypair**: generated at registration. The **private** half
|
||||||
|
is wrapped under the account DEK (so it unlocks at login); the **public**
|
||||||
|
half is stored in plaintext — public keys are not secret.
|
||||||
|
|
||||||
|
This adds one tier to today's flat `account DEK → data`. Rationale in
|
||||||
|
Consequences (§3-tier vs flat).
|
||||||
|
|
||||||
|
### 2. Ownership and access
|
||||||
|
|
||||||
|
- Each **profile** has exactly one **owning account** at a time. The owner
|
||||||
|
holds the profile DEK wrapped to their account DEK (the *owner share*).
|
||||||
|
- **Shared access** = the profile DEK wrapped to another account's X25519
|
||||||
|
public key. Stored on the server as a *recipient share*, indexed by
|
||||||
|
`(profileId, recipientUserId)`.
|
||||||
|
- To **read** a shared profile, the recipient's client (at login) unwraps their
|
||||||
|
X25519 private key, ECDH-derives a wrapping key against the share's ephemeral
|
||||||
|
public key, and unwraps the profile DEK.
|
||||||
|
|
||||||
|
This is the standard asymmetric-envelope pattern (cf. Signal, `age`,
|
||||||
|
Tarsnap). The server only ever stores wrapped blobs and public keys.
|
||||||
|
|
||||||
|
### 3. Sharing flow (owner → recipient)
|
||||||
|
|
||||||
|
Client-side only, server untrusted:
|
||||||
|
|
||||||
|
1. Owner's client looks up the **recipient's X25519 public key**
|
||||||
|
(`GET /api/users/:id/public-key`, new).
|
||||||
|
2. Owner's client unwraps the target profile's DEK (via their account DEK).
|
||||||
|
3. Generate an **ephemeral X25519 keypair** for this share.
|
||||||
|
4. ECDH(ephemeral priv, recipient pub) → AES-GCM **wrapping key**.
|
||||||
|
5. Wrap the profile DEK under that wrapping key → `{ciphertext, iv, ephemeralPubKey}`.
|
||||||
|
6. `POST /api/profiles/:id/shares` with `{recipientUserId, wrappedDek, ephemeralPubKey, permissions, expiresAt?}`.
|
||||||
|
7. Server stores the share record. It cannot decrypt it.
|
||||||
|
|
||||||
|
### 4. Revocation
|
||||||
|
|
||||||
|
Two layers, in order of strength:
|
||||||
|
|
||||||
|
- **Soft revoke** (cheap): delete or deactivate the recipient share record. The
|
||||||
|
recipient's client no longer receives the wrapped DEK from the server, so it
|
||||||
|
cannot decrypt **new reads**. Caveat: a malicious client that cached the
|
||||||
|
profile DEK before revocation keeps decrypting data it already fetched.
|
||||||
|
- **Hard revoke / re-key** (strong): owner generates a **new profile DEK**,
|
||||||
|
re-encrypts all of the profile's data under it, re-wraps to the owner share
|
||||||
|
and to all *still-valid* recipient shares. A revoked recipient's cached old
|
||||||
|
DEK now fails to decrypt any future fetch. Required for the "kid revokes
|
||||||
|
parent" graduation case where you must assume the parent may have cached
|
||||||
|
data.
|
||||||
|
|
||||||
|
Hard revocation is the answer to requirement (3). Re-keying is expensive
|
||||||
|
(linear in profile data size) so it is opt-in / triggered explicitly, not the
|
||||||
|
default for every revoke.
|
||||||
|
|
||||||
|
### 5. Profile model changes
|
||||||
|
|
||||||
|
To support pets and graduation, the current `role: "patient"` string becomes:
|
||||||
|
|
||||||
|
```
|
||||||
|
kind: "human" | "pet" // distinguishes people from animals
|
||||||
|
relationship: string // freeform: "self", "child", "spouse", "pet", ...
|
||||||
|
```
|
||||||
|
|
||||||
|
Ownership-transfer (graduation) is **not new crypto** — it is the existing
|
||||||
|
share machinery plus an `owner_account_id` field swap:
|
||||||
|
|
||||||
|
1. New adult account (the graduated child) generates its X25519 keypair.
|
||||||
|
2. Old owner wraps the profile DEK to the new account's pubkey as a share, and
|
||||||
|
marks that share as the new **owner share**.
|
||||||
|
3. Old owner's access is downgraded to (or removed as) a normal recipient share.
|
||||||
|
4. The new owner can revoke any remaining parent shares via soft/hard revoke.
|
||||||
|
|
||||||
|
### 5b. Use case: elderly parent cared for by children
|
||||||
|
|
||||||
|
A key motivation for the profile/account split is that **profile ownership and
|
||||||
|
account login are independent.** The elderly-care scenario has three setups,
|
||||||
|
all covered:
|
||||||
|
|
||||||
|
- **Parent self-manages, shares to children.** Parent owns their own account
|
||||||
|
and `self` profile; creates recipient shares to each child. Standard
|
||||||
|
share-to-another-account flow. Any party can revoke.
|
||||||
|
- **Managing child holds a dedicated parent account.** A separate account
|
||||||
|
exists for the parent (the identity of record), but the managing child holds
|
||||||
|
or controls the password and shares the profile to siblings. Cleaner than
|
||||||
|
embedding the parent as a profile inside the child's account, and preserves
|
||||||
|
the "take over later" path: if the parent becomes able (or, conversely,
|
||||||
|
passes away), it's the same ownership machinery.
|
||||||
|
- **Parent modeled as a profile inside the managing child's account.** No
|
||||||
|
separate login for the parent; the child owns the profile and shares it.
|
||||||
|
Simplest, but the parent has no independent identity and cannot revoke
|
||||||
|
themselves. Recommend against this when the parent is or may become an
|
||||||
|
independent actor.
|
||||||
|
|
||||||
|
**Death/incapacity of the sole-account owner** is the one wrinkle that needs
|
||||||
|
an explicit decision: see "Permissions and the admin-share question" below.
|
||||||
|
|
||||||
|
### 5c. Permissions and the admin-share question
|
||||||
|
|
||||||
|
The `permissions` list on a recipient share (Open question 3) raises a
|
||||||
|
sub-decision: **does an `admin` permission let a recipient act as a quasi-owner
|
||||||
|
without the original owner?** Specifically: re-share the profile, re-key it
|
||||||
|
(hard revoke), and add/remove other shares.
|
||||||
|
|
||||||
|
This matters for two scenarios:
|
||||||
|
|
||||||
|
- **Graduation in reverse / death of the owner.** If the parent owns the
|
||||||
|
account and becomes incapacitated, no child can write, re-share, or revoke —
|
||||||
|
those require the owner's account DEK, which is gone with the password.
|
||||||
|
- **Operational delegation.** A primary caregiver wants to delegate admin
|
||||||
|
tasks without sharing their password.
|
||||||
|
|
||||||
|
**Decision: support an `admin` permission tier.** An admin share can re-share,
|
||||||
|
re-key, and manage other shares on a profile. This keeps the owner-DEK model
|
||||||
|
intact (the owner share is still special — it can *transfer* ownership, which
|
||||||
|
admin cannot) while handling the death/incapacity and delegation cases without
|
||||||
|
key escrow. Hard revocation (re-key) by an admin rotates the profile DEK and
|
||||||
|
re-wraps to all *still-valid* shares, same as owner-initiated re-key.
|
||||||
|
|
||||||
|
> ❓ **Open sub-item:** when an admin re-keys, do we require a quorum or a
|
||||||
|
> second admin's consent? For MVP, no — any admin can act alone, matching how
|
||||||
|
> ownership transfer works today. Revisit if multi-party control becomes a
|
||||||
|
> requirement.
|
||||||
|
|
||||||
|
### 6. What happens to the current `Share` model
|
||||||
|
|
||||||
|
The existing `backend/src/models/share.rs` is **resource-level ACL**
|
||||||
|
(`resource_type`, `resource_id`, `permissions`) and predates encryption. It is
|
||||||
|
incompatible with this design, which shares at the **profile** level via keys,
|
||||||
|
not at the resource level via permissions.
|
||||||
|
|
||||||
|
Per issue #3, profile-level sharing covers the family/caregiver need; the
|
||||||
|
"share one document" need is handled separately (physician PDF feature). So:
|
||||||
|
|
||||||
|
- The current `Share` model and `/api/shares` routes are **retained for now**
|
||||||
|
but treated as a placeholder (issue #3). They will be either removed or
|
||||||
|
repurposed; this ADR does not depend on them.
|
||||||
|
- The new recipient-share records live in a **new collection** (provisional
|
||||||
|
name `profile_shares`) keyed by `(profileId, recipientUserId)`. Storing
|
||||||
|
wrapped DEKs, not ACLs.
|
||||||
|
|
||||||
|
### 7. Server surface (new endpoints, indicative)
|
||||||
|
|
||||||
|
- `GET /api/users/:id/public-key` → recipient's X25519 public key (plaintext).
|
||||||
|
- `GET /api/profiles/me/dek` → the account-wrapped profile DEK (owner unlock).
|
||||||
|
- `POST /api/profiles` → create profile: client generates profile DEK, wraps to
|
||||||
|
account DEK, sends wrapped form. Returns `profileId`.
|
||||||
|
- `GET /api/profiles/:id/shares` → list recipient shares for a profile.
|
||||||
|
- `POST /api/profiles/:id/shares` → create a recipient share (wrapped DEK).
|
||||||
|
- `DELETE /api/profiles/:id/shares/:shareId` → soft revoke.
|
||||||
|
- `POST /api/profiles/:id/rekey` → hard revoke / rotate the profile DEK.
|
||||||
|
|
||||||
|
All endpoints store/return opaque blobs or public keys; none expose a DEK or a
|
||||||
|
private key.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
### Positive
|
||||||
|
|
||||||
|
- **Satisfies all four product constraints.** Per-profile keys enable the
|
||||||
|
divorced-parents case; hard revoke enables the revocation requirement; the
|
||||||
|
X25519 envelope keeps the server untrusted.
|
||||||
|
- **Profiles become first-class subjects of care** — owners, shares, pets,
|
||||||
|
graduation all fall out naturally.
|
||||||
|
- **Server stays a blind store.** No new backend crypto; still no `aes`/crypto
|
||||||
|
crates in `Cargo.toml`.
|
||||||
|
- **Composition with existing recovery.** The account DEK is still recovered
|
||||||
|
via the existing password/recovery wrapped-DEK flow; profile DEKs are wrapped
|
||||||
|
to the account DEK, so recovering the account recovers all profiles.
|
||||||
|
|
||||||
|
### Negative / costs
|
||||||
|
|
||||||
|
- **Adds a key tier.** Three levels instead of one. Complexity in the client
|
||||||
|
crypto module (`crypto/keys.ts` will grow X25519 + wrap-to-recipient logic).
|
||||||
|
- **Migration.** Today all data is under one account DEK. Splitting into
|
||||||
|
per-profile DEKs requires reading, decrypting, re-encrypting, and re-uploading
|
||||||
|
every document. **There is no real user data yet** (per
|
||||||
|
`zero-knowledge-encryption.md`), so this is cheap **now** and expensive
|
||||||
|
later — strong argument for doing the re-key before launch.
|
||||||
|
- **Hard revocation is expensive.** Re-keying a profile is O(profile data
|
||||||
|
size). Mitigation: soft revoke by default, hard revoke only when the owner
|
||||||
|
explicitly requires it (e.g. graduation, suspected compromise).
|
||||||
|
- **Recipient must have an account** (constraint 2). No magic-link sharing in
|
||||||
|
this design — deferred to the physician-PDF feature.
|
||||||
|
|
||||||
|
### Security notes
|
||||||
|
|
||||||
|
- **Ephemeral keys per share** prevent cross-share correlation and give
|
||||||
|
forward secrecy within a share's lifetime (re-keying rotates the DEK).
|
||||||
|
- **ECDH + AES-GCM** is the Web Crypto idiom; X25519 keys are 32 bytes, shares
|
||||||
|
are small.
|
||||||
|
- **Public keys are not authenticated by the server.** The owner must trust
|
||||||
|
that `GET /users/:id/public-key` returns the right key. Since the server is
|
||||||
|
untrusted, this is an accepted risk: a malicious server could swap the key
|
||||||
|
and force a re-share, but cannot decrypt. ❓ Decide whether to add a key
|
||||||
|
fingerprint the recipient verifies out-of-band.
|
||||||
|
|
||||||
|
## Open questions (❓) — resolved 2026-07-18
|
||||||
|
|
||||||
|
1. **Key hierarchy depth.** ✅ **Decided: 3-tier** (account DEK → profile DEK →
|
||||||
|
data). Rationale: account DEK is recovered via the existing wrapped-DEK
|
||||||
|
recovery flow, so password change only re-wraps the account DEK (O(1)),
|
||||||
|
not every profile DEK. Profile DEKs are re-wrapped only on profile-level
|
||||||
|
events (share/revoke/rekey), which are rare.
|
||||||
|
|
||||||
|
2. **Public-key trust.** ✅ **Decided: accept key-swap as DoS-only for now.**
|
||||||
|
A malicious server can swap a recipient's public key and force a failed
|
||||||
|
share (DoS) but cannot decrypt. **Future enhancement (not blocking):** add
|
||||||
|
a mechanism for out-of-band pubkey verification. Candidate approaches to
|
||||||
|
evaluate when we get there:
|
||||||
|
- **Key fingerprint / safety number.** Display a short hash of a user's
|
||||||
|
pubkey that two parties can compare out-of-band (cf. Signal safety
|
||||||
|
numbers). Cheap, requires user action.
|
||||||
|
- **Pubkey in recovery phrase / account export.** Derive or include the
|
||||||
|
pubkey fingerprint in something the user already verifies.
|
||||||
|
- **Web-of-trust / TOFU.** First-seen pubkey is pinned client-side; a change
|
||||||
|
triggers a user-visible warning.
|
||||||
|
These are additive — none changes the core envelope design, so they can
|
||||||
|
ship later without a migration.
|
||||||
|
|
||||||
|
3. **Share record schema.** ✅ **Decided: `profile_shares` collection, schema
|
||||||
|
as proposed, permissions included.** Fields:
|
||||||
|
`{profileId, recipientUserId, ephemeralPubKey, wrappedDek, iv, permissions,
|
||||||
|
expiresAt, active, createdAt}`. `permissions` is a list (e.g.
|
||||||
|
`["read"]`, `["read","write"]`, `["read","write","admin"]`); the server
|
||||||
|
enforces write/admin server-side by checking the share record (the owner
|
||||||
|
always implicitly has admin via the owner share). See §"Permissions and
|
||||||
|
the admin-share question" below for the semantics of `admin`.
|
||||||
|
|
||||||
|
4. **Migration.** ✅ **Decided: no migration — wipe the database.** There is
|
||||||
|
no real user data, and the re-key cost is unjustified. Phase A therefore
|
||||||
|
includes a clean-cutover step rather than a per-document migration. (This
|
||||||
|
also removes any legacy single-DEK documents that would otherwise need
|
||||||
|
special-casing.)
|
||||||
|
|
||||||
|
5. **Existing `Share` model fate.** ✅ **Decided: remove entirely.**
|
||||||
|
`backend/src/models/share.rs`, the `/api/shares` routes, and the
|
||||||
|
`permissions/check` endpoint are deleted; the new `profile_shares`
|
||||||
|
collection and `/api/profiles/:id/shares` endpoints replace them. The
|
||||||
|
future per-resource physician-share feature will be built from scratch on
|
||||||
|
its own model when prioritized (it's a different access pattern — one-way
|
||||||
|
document dump, not ongoing profile access).
|
||||||
|
|
||||||
|
## Phasing (proposal)
|
||||||
|
|
||||||
|
- **Phase A — key tiers without sharing.** Introduce per-profile DEKs and the
|
||||||
|
account X25519 keypair; migrate existing data; no sharing endpoints yet.
|
||||||
|
Unblocks multi-profile-within-account (the parent/child single-login case).
|
||||||
|
- **Phase B — profile sharing.** The `POST/GET/DELETE /profiles/:id/shares`
|
||||||
|
endpoints and soft revoke.
|
||||||
|
- **Phase C — hard revoke / re-key.** The `/profiles/:id/rekey` endpoint and
|
||||||
|
owner-driven DEK rotation.
|
||||||
|
- **Phase D — graduation / ownership transfer.** Owner-share swap UI + endpoint.
|
||||||
|
|
||||||
|
Each phase is independently shippable and independently testable.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- Issue: [#3](https://gitea.soliverez.com.ar/alvaro/normogen/issues/3)
|
||||||
|
- Predecessor ADR: [`zero-knowledge-encryption.md`](./zero-knowledge-encryption.md)
|
||||||
|
- Current client crypto: `web/normogen-web/src/crypto/keys.ts`, `cipher.ts`
|
||||||
|
- Profile model: `backend/src/models/profile.rs`
|
||||||
|
- (Incompatible) current share model: `backend/src/models/share.rs`
|
||||||
|
- Web Crypto API: X25519 via `generateKey({name:"ECDH", namedCurve:"X25519"})`,
|
||||||
|
`deriveBits` for ECDH, `wrapKey`/`unwrapKey` or manual AES-GCM for the envelope.
|
||||||
|
|
@ -1,5 +1,13 @@
|
||||||
# Encryption.md Update Summary
|
# Encryption.md Update Summary
|
||||||
|
|
||||||
|
> **ARCHIVED 2026-07-18.** This was a changelog note for a 2026-03-09 rewrite of
|
||||||
|
> `docs/product/encryption.md` that itself became stale (it describes ZK
|
||||||
|
> encryption as "planned" when it is in fact implemented). Kept here for
|
||||||
|
> history only. The current, accurate encryption doc is
|
||||||
|
> [`docs/product/encryption.md`](../product/encryption.md), and the canonical
|
||||||
|
> design is [`docs/adr/zero-knowledge-encryption.md`](../adr/zero-knowledge-encryption.md).
|
||||||
|
> See issue #4.
|
||||||
|
|
||||||
**Date**: 2026-03-09
|
**Date**: 2026-03-09
|
||||||
**File**: docs/product/encryption.md
|
**File**: docs/product/encryption.md
|
||||||
**Update**: Added Rust implementation examples and current status
|
**Update**: Added Rust implementation examples and current status
|
||||||
|
|
@ -1,10 +1,16 @@
|
||||||
# Persona and Family Management - Product Definition
|
# Persona and Family Management - Product Definition
|
||||||
|
|
||||||
**Document Version**: 1.0
|
**Document Version**: 1.1
|
||||||
**Date**: 2026-03-09
|
**Date**: 2026-07-18 (reconciled with code; original draft 2026-03-09)
|
||||||
**Status**: Draft - For Refinement
|
**Status**: Vision doc — implementation status section corrected to match code
|
||||||
**Purpose**: Consolidate all current information about persona and family management features for further product refinement
|
**Purpose**: Consolidate all current information about persona and family management features for further product refinement
|
||||||
|
|
||||||
|
> **How to read this doc.** Sections marked **Vision** describe the target
|
||||||
|
> product and are still valid for discussion. Sections marked **Today (code)**
|
||||||
|
> describe what is actually implemented and have been verified against the
|
||||||
|
> codebase as of 2026-07-18. Where the two diverge, the gap is called out and
|
||||||
|
> linked to the relevant Forgejo issue.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 📋 Table of Contents
|
## 📋 Table of Contents
|
||||||
|
|
@ -35,6 +41,28 @@ Normogen supports **multi-person health data management** through a persona and
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Current reality (as of 2026-07-18)
|
||||||
|
|
||||||
|
This section corrects the earlier "Current Implementation Status" section below,
|
||||||
|
which described an earlier state. Verified against the code:
|
||||||
|
|
||||||
|
| Capability | Status | Notes |
|
||||||
|
|---|---|---|
|
||||||
|
| Single profile per user account | ✅ Done | `GET/PUT /api/profiles/me`; profile is auto-created as `profile_{user_id}` at registration. Display name is client-encrypted. |
|
||||||
|
| Multi-profile (child/dependent) within one account | ❌ Not built | `ProfileRepository::find_by_user_id` returns a single profile; no create/list/switch endpoints. `profileId` can be tagged onto medications but there is no backing entity beyond the owner's. |
|
||||||
|
| Family groups | ❌ Not built | `Family` model exists but has **no handler and no routes**. JWT carries `family_id` but nothing populates or checks it. |
|
||||||
|
| Cross-user sharing (spouse, caregiver) | ❌ **Blocked by design** | `/api/shares` routes exist but cannot grant decryption access — all data is encrypted under the owner's DEK and there is no key-distribution mechanism. See **[issue #3](https://gitea.soliverez.com.ar/alvaro/normogen/issues/3)**. |
|
||||||
|
| Caregiver roles / permissions | ❌ Not built | `permissions` field exists on Profile and in JWT claims but is not enforced. |
|
||||||
|
| Zero-knowledge encryption of health data | ✅ Done | Client-side Web Crypto, wrapped-DEK recovery. See [`encryption.md`](encryption.md). |
|
||||||
|
|
||||||
|
**Implication for the family/caregiver personas below:** the single biggest
|
||||||
|
open question is architectural — *how does sharing work under zero-knowledge?*
|
||||||
|
That must be decided before family/caregiver features can be implemented. Until
|
||||||
|
then, the realistic near-term scope is **multi-profile within a single account**
|
||||||
|
(one login managing several people's data, no cross-account sharing).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## User Personas
|
## User Personas
|
||||||
|
|
||||||
### Primary: Privacy-Conscious Individual
|
### Primary: Privacy-Conscious Individual
|
||||||
|
|
@ -167,7 +195,12 @@ User Account: jane.doe@example.com (Jane)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Current Implementation Status
|
## Current Implementation Status (superseded — see "Current reality" above)
|
||||||
|
|
||||||
|
> The detail below was written 2026-03-09 against an earlier state and
|
||||||
|
> overstates what existed then. It is retained for history. For the accurate
|
||||||
|
> current picture, read the **Current reality** section near the top of this
|
||||||
|
> document, and [`encryption.md`](encryption.md) for encryption.
|
||||||
|
|
||||||
### ✅ Implemented (Backend)
|
### ✅ Implemented (Backend)
|
||||||
|
|
||||||
|
|
@ -595,16 +628,23 @@ if profile.role == "child" || profile.age < 18 {
|
||||||
|
|
||||||
### Encryption
|
### Encryption
|
||||||
|
|
||||||
**Zero-Knowledge Encryption**:
|
> **Correction (2026-07-18):** the bullets below described a planned
|
||||||
- Profile names are encrypted (AES-256-GCM)
|
> server-side scheme that was never built. The shipped system is
|
||||||
- Family names are encrypted
|
> **client-side** zero-knowledge via the browser's Web Crypto API with a
|
||||||
- Only users with correct password can decrypt
|
> wrapped-DEK model. The server has no crypto code at all — it is a blind
|
||||||
- Server cannot see profile names
|
> store. Profile/medication/appointment data is encrypted in the browser
|
||||||
|
> before upload and the server cannot read any of it. Full details:
|
||||||
|
> [`encryption.md`](encryption.md) and
|
||||||
|
> [`adr/zero-knowledge-encryption.md`](../adr/zero-knowledge-encryption.md).
|
||||||
|
|
||||||
**Encrypted Fields**:
|
**What's actually encrypted (client-side, today):**
|
||||||
- `profile.name` - Person's name
|
- Medication data blob (name, dosage, frequency, route, notes, …)
|
||||||
- `family.name` - Family group name
|
- Appointment data blob (title, provider, date/time, …)
|
||||||
- Medication data (when implemented)
|
- Profile display name
|
||||||
|
|
||||||
|
**What stays plaintext (queryable by the server):**
|
||||||
|
- IDs (`profileId`, `medicationId`, `userId`, `appointmentId`)
|
||||||
|
- Medication `active` flag, appointment `status`, `doseSchedule`, timestamps
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
Loading…
Add table
Add a link
Reference in a new issue