Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,9 +101,9 @@ type Crypter interface {
### Data Flow
1. User submits message + PIN via web UI or API
2. Server validates input (PIN size, expiration limits)
3. MessageProc hashes PIN with bcrypt, encrypts message data
4. Encrypted message saved to storage engine with UUID key and expiration
5. For retrieval: validate PIN attempts, decrypt if correct, delete after successful read
3. MessageProc hashes PIN with bcrypt; API messages are encrypted server-side, UI messages arrive already encrypted by the browser and are stored as-is
4. Encrypted message saved to storage engine with 12-character base62 key and expiration
5. For retrieval: validate PIN attempts, then delete and return; API messages are decrypted server-side first, UI messages are handed back as stored ciphertext for the browser to decrypt

### File Message Format
File messages use a distinct storage format with encrypted metadata:
Expand Down Expand Up @@ -183,7 +183,7 @@ When `--allow-no-pin` / `ALLOW_NO_PIN` is enabled, secrets can be created withou
- **rest.RealIP middleware** - Extracts client IP from headers for CDN/proxy compatibility (from go-pkgz/rest v1.20.6+)
- Header priority: X-Real-IP → CF-Connecting-IP → leftmost public IP in X-Forwarded-For → RemoteAddr
- Filters private/loopback/link-local IPs automatically
- **HashedIP middleware** - Anonymizes client IP using HMAC-SHA1 hash (12-char hex) for audit logging
- **HashedIP middleware** - Anonymizes client IP using HMAC-SHA256 hash (8-char hex) for audit logging
- Must run after rest.RealIP middleware (reads `r.RemoteAddr` set by RealIP)
- **Logger middleware** - Logs requests with masked sensitive paths (PINs) and anonymized IPs
- Must run after HashedIP middleware (reads hashed IP from context)
Expand Down
14 changes: 7 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,11 +84,11 @@ The service uses **hybrid encryption** based on how you access it:
### Security Architecture

**Encryption:**
- Server-side: AES-256-GCM for message encryption
- Server-side: NaCl secretbox (XSalsa20-Poly1305) for message encryption
- Client-side (web UI): AES-128-GCM via Web Crypto API
- PIN hashing: bcrypt (cost 14)
- Random key generation: 32-byte cryptographically secure
- Server rejects unencrypted content from web clients (ciphertext format validation)
- PIN hashing: bcrypt (`bcrypt.DefaultCost`, currently 10)
- Random generation: 128-bit client keys and 12-character base62 message IDs, both from a CSPRNG
- Server checks that content from web clients has the shape of a ciphertext envelope (base64url, at least IV plus tag)

**HTTP Security Headers:**
- `Content-Security-Policy`: restricts scripts, styles, fonts to trusted sources; `frame-ancestors 'none'`; `form-action 'self'`
Expand Down Expand Up @@ -384,7 +384,7 @@ $ curl -X POST https://safesecret.info/api/v1/message \

{
"exp": "2024-01-15T10:30:00Z",
"key": "f1acfe04-277f-4016-518d-16c312ab84b5"
"key": "u2yKL07dDtRR"
}
```

Expand All @@ -395,10 +395,10 @@ GET /api/v1/message/:key/:pin
```

```bash
$ curl https://safesecret.info/api/v1/message/f1acfe04-277f-4016-518d-16c312ab84b5/12345
$ curl https://safesecret.info/api/v1/message/u2yKL07dDtRR/12345

{
"key": "f1acfe04-277f-4016-518d-16c312ab84b5",
"key": "u2yKL07dDtRR",
"message": "my secret"
}
```
Expand Down
2 changes: 1 addition & 1 deletion app/messager/crypt.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ func (c Crypt) Decrypt(req Request) ([]byte, error) {
return decrypted, nil
}

// MakeSignKey creates 32-pin bytes signKey for AES256
// MakeSignKey creates 32-pin bytes signKey for secretbox
func MakeSignKey(signKey string, pinSize int) (result string) {
if len(signKey) >= 32-pinSize {
return signKey[:32-pinSize]
Expand Down