mirror of
https://github.com/cupcakearmy/cryptgeon.git
synced 2026-09-27 04:51:45 +00:00
Compare commits
21
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2dc9d6aa16 | ||
|
|
98c56f6fb9 | ||
|
|
3f7bb6f32e | ||
|
|
6de97039d3 | ||
|
|
a0bca0a1a7 | ||
|
|
41d0f0bfa2 | ||
|
|
ba7669514e | ||
|
|
8a3831fd05 | ||
|
|
56f8498530 | ||
|
|
c3c6e96774 | ||
|
|
b5c5629cca | ||
|
|
74f387f920 | ||
|
|
b7c20d6f59 | ||
|
|
b0737030b2 | ||
|
|
918640e54c | ||
|
|
7a68422d67 | ||
|
|
8162db4364 | ||
|
|
f36ae73b42 | ||
|
|
2ded578141 | ||
|
|
82900adef8 | ||
|
|
8871a6b90d |
@@ -23,7 +23,13 @@ jobs:
|
|||||||
pnpm install
|
pnpm install
|
||||||
pnpm --filter cryptgeon build
|
pnpm --filter cryptgeon build
|
||||||
|
|
||||||
- run: pnpm publish --filter cryptgeon
|
- name: Publish to npm
|
||||||
|
run: |
|
||||||
|
DIST_TAG=latest
|
||||||
|
if [[ "${GITHUB_REF_NAME}" == *"-"* ]]; then
|
||||||
|
DIST_TAG=rc
|
||||||
|
fi
|
||||||
|
pnpm publish --filter cryptgeon --tag "${DIST_TAG}" --no-git-checks
|
||||||
env:
|
env:
|
||||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||||
|
|
||||||
|
|||||||
+196
@@ -5,6 +5,183 @@ All notable changes to this project will be documented in this file.
|
|||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## [Unreleased] — v3 (major rewrite)
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- New shared TypeScript package `@cryptgeon/shared` as single source of truth for crypto, content codec and API client (crypto + compression + payload + types).
|
||||||
|
- Shared payload codec: `packContent` / `unpackContent` (encode → LZ4 → XChaCha20-Poly1305 and reverse).
|
||||||
|
- New `pg`-backend storage of note hashes in the cache.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Encryption from AES to **XChaCha20-Poly1305** (client-side); dropped `occulto`.
|
||||||
|
- All API bodies switched to **MessagePack**.
|
||||||
|
- Frontend migrated to SvelteKit + `@cryptgeon/shared`.
|
||||||
|
- CLI rebuilt with `vite-plus` (bundles all deps) and imports from `@cryptgeon/shared`.
|
||||||
|
|
||||||
|
### Breaking changes
|
||||||
|
|
||||||
|
- Endpoints moved to `/api/v3/notes/` and `/api/v3/status`; health check to `/healthz`.
|
||||||
|
- `meta.extra` holds client-opaque data (e.g. scrypt derivation params), size-limited (`EXTRA_SIZE_LIMIT`, default 512 bytes).
|
||||||
|
- Inner payload is msgpack: `{ type: "text", data }` or `{ type: "files", data: [{ name, mime, size, data }] }`.
|
||||||
|
- Env renames: `REDIS` → `CACHE`, `REDIS_PREFIX` → `CACHE_PREFIX`; new `EXTRA_SIZE_LIMIT`.
|
||||||
|
- Docker `redis` service → `cache`; healthcheck → `http://127.0.0.1:8000/healthz`; image stays `valkey/valkey:7-alpine` (swap for any RESP-compatible).
|
||||||
|
- Storage switched to cache hashes with atomic `HINCRBY` view counting; the per-note lock (`lock.rs`) is removed.
|
||||||
|
- Notes can have **both** `views` and `expiration` set simultaneously.
|
||||||
|
- v2 notes are **not migrated**: flush the cache before deploying v3; v2/v3 notes are not interoperable.
|
||||||
|
|
||||||
|
## [2.9.3] - 2026-06-25
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Basic file drag-and-drop support.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Publish the Docker image to GitHub Container Registry (ghcr).
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- #207: keep audio/other file mime types intact.
|
||||||
|
- Localization key typo `note_to_big` → `note_too_big`.
|
||||||
|
|
||||||
|
## [2.9.2] - 2026-06-07
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Image paste support.
|
||||||
|
- Czech translation.
|
||||||
|
- `THEME_HOME_LINK` environment variable.
|
||||||
|
- Docker compose: prevent anonymous volume creation.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Replace Redis with Valkey in docker-compose files.
|
||||||
|
- Rust 2024 edition compat, watchexec and axum 0.8 updates.
|
||||||
|
- Switched license checker package.
|
||||||
|
- Frontend cleanup and readme/docs cleanup.
|
||||||
|
|
||||||
|
### Security
|
||||||
|
|
||||||
|
- Updated dependencies (ring, npm_and_yarn group).
|
||||||
|
|
||||||
|
## [2.9.1] - 2025-02-27
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Docs about running Redis in RAM-only mode.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Password eye toggle not working.
|
||||||
|
|
||||||
|
### Security
|
||||||
|
|
||||||
|
- Updated dependencies.
|
||||||
|
|
||||||
|
## [2.9.0] - 2025-01-18
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Frontend rework: migrate to Svelte 5.
|
||||||
|
- Update Redis documentation link in compose.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fix race condition on the delete endpoint by introducing locks to guarantee the view counter.
|
||||||
|
|
||||||
|
## [2.8.4] - 2025-01-02
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Chinese (zh-TW) translations.
|
||||||
|
- Basic auth example (nginx).
|
||||||
|
|
||||||
|
## [2.8.3] - 2024-09-27
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Options to add an imprint: `IMPRINT_URL`, `IMPRINT_HTML`.
|
||||||
|
|
||||||
|
## [2.8.2] - 2024-09-20
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Raycast extension links.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Add `type="button"` to form elements.
|
||||||
|
- Bump pnpm version.
|
||||||
|
|
||||||
|
## [2.8.1] - 2024-09-02
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Move shared package into the CLI.
|
||||||
|
- Add a guide.
|
||||||
|
|
||||||
|
## [2.8.0] - 2024-08-27
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Migrate backend from actix to axum (major refactor).
|
||||||
|
- More robust config, body limit via axum.
|
||||||
|
- Use container for test pipeline; skip size/expiration quirks in Safari.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Typos in English localization.
|
||||||
|
|
||||||
|
## [2.7.0] - 2024-08-23
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Better programmatic access to the shared client.
|
||||||
|
- Redis TLS feature, dynamically-linked and native musl targets.
|
||||||
|
- French blog post and improved French translations.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Bump redis crate to 0.25.2.
|
||||||
|
|
||||||
|
## [2.6.1] - 2024-05-04
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Polish translation.
|
||||||
|
|
||||||
|
## [2.6.0] - 2024-03-24
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- `ALLOW_FILES` flag.
|
||||||
|
- `NEW_NOTE_NOTICE` → `THEME_NEW_NOTE_NOTICE` theme flag.
|
||||||
|
- French translation update.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Reset form when clicking the logo after creating a note.
|
||||||
|
|
||||||
|
## [2.5.1] - 2024-03-04
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Reset translation.
|
||||||
|
- German (`de`) translation update.
|
||||||
|
|
||||||
|
## [2.5.0] - 2024-03-04
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Expose internal shared functionality for external/programmatic usage.
|
||||||
|
- German translation updates.
|
||||||
|
|
||||||
|
### Security
|
||||||
|
|
||||||
|
- Updated dependencies (zerocopy).
|
||||||
|
|
||||||
## [2.4.0] - 2023-11-01
|
## [2.4.0] - 2023-11-01
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
@@ -12,6 +189,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
- Removed HTML sanitation, display the original message as string
|
- Removed HTML sanitation, display the original message as string
|
||||||
- Links are now displayed under the note in a separate section
|
- Links are now displayed under the note in a separate section
|
||||||
|
|
||||||
|
## [2.3.3] - 2023-08-15
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Maintenance.
|
||||||
|
- Updated dependencies.
|
||||||
|
|
||||||
|
## [2.3.2] - 2023-08-04
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Spanish readme (`README_ES.md`).
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Translation and grammar fixes (en, de, de, es).
|
||||||
|
|
||||||
## [2.3.1] - 2023-06-23
|
## [2.3.1] - 2023-06-23
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
@@ -30,6 +224,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
- Moved to monorepo.
|
- Moved to monorepo.
|
||||||
|
|
||||||
|
## [2.2.0] - 2023-01-14
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
- Default port is now 8000, not 5000.
|
- Default port is now 8000, not 5000.
|
||||||
|
|||||||
@@ -1,614 +0,0 @@
|
|||||||
{
|
|
||||||
"info": {
|
|
||||||
"_postman_id": "3aaeac19-4eac-4911-b3c8-912b17a48634",
|
|
||||||
"name": "Cryptgeon",
|
|
||||||
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
|
|
||||||
},
|
|
||||||
"item": [
|
|
||||||
{
|
|
||||||
"name": "Notes",
|
|
||||||
"item": [
|
|
||||||
{
|
|
||||||
"name": "Preview",
|
|
||||||
"request": {
|
|
||||||
"method": "GET",
|
|
||||||
"header": [],
|
|
||||||
"url": {
|
|
||||||
"raw": "{{BASE}}/notes/:id",
|
|
||||||
"host": ["{{BASE}}"],
|
|
||||||
"path": ["notes", ":id"],
|
|
||||||
"variable": [
|
|
||||||
{
|
|
||||||
"key": "id",
|
|
||||||
"value": "{{NOTE_ID}}",
|
|
||||||
"description": "Id of the Note"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"description": "This endpoint is to query wether a note exists, without actually opening it. No view limits are used here, as contents of the note are not available, only the `meta` field is returned, which is public."
|
|
||||||
},
|
|
||||||
"response": [
|
|
||||||
{
|
|
||||||
"name": "200",
|
|
||||||
"originalRequest": {
|
|
||||||
"method": "GET",
|
|
||||||
"header": [],
|
|
||||||
"url": {
|
|
||||||
"raw": "{{BASE}}/notes/:id",
|
|
||||||
"host": ["{{BASE}}"],
|
|
||||||
"path": ["notes", ":id"],
|
|
||||||
"variable": [
|
|
||||||
{
|
|
||||||
"key": "id",
|
|
||||||
"value": "{{NOTE_ID}}",
|
|
||||||
"description": "Id of the Note"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"status": "OK",
|
|
||||||
"code": 200,
|
|
||||||
"_postman_previewlanguage": "json",
|
|
||||||
"header": [
|
|
||||||
{
|
|
||||||
"key": "transfer-encoding",
|
|
||||||
"value": "chunked"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "connection",
|
|
||||||
"value": "close"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "content-type",
|
|
||||||
"value": "application/json"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "content-encoding",
|
|
||||||
"value": "gzip"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "vary",
|
|
||||||
"value": "accept-encoding"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "date",
|
|
||||||
"value": "Tue, 23 May 2023 05:24:29 GMT"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"cookie": [],
|
|
||||||
"body": "{}"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "404",
|
|
||||||
"originalRequest": {
|
|
||||||
"method": "GET",
|
|
||||||
"header": [],
|
|
||||||
"url": {
|
|
||||||
"raw": "{{BASE}}/notes/:id",
|
|
||||||
"host": ["{{BASE}}"],
|
|
||||||
"path": ["notes", ":id"],
|
|
||||||
"variable": [
|
|
||||||
{
|
|
||||||
"key": "id",
|
|
||||||
"value": "{{NOTE_ID}}",
|
|
||||||
"description": "Id of the Note"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"status": "Not Found",
|
|
||||||
"code": 404,
|
|
||||||
"_postman_previewlanguage": "plain",
|
|
||||||
"header": [
|
|
||||||
{
|
|
||||||
"key": "transfer-encoding",
|
|
||||||
"value": "chunked"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "connection",
|
|
||||||
"value": "close"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "vary",
|
|
||||||
"value": "accept-encoding"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "content-encoding",
|
|
||||||
"value": "gzip"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "date",
|
|
||||||
"value": "Tue, 23 May 2023 05:25:26 GMT"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"cookie": [],
|
|
||||||
"body": ""
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Create",
|
|
||||||
"event": [
|
|
||||||
{
|
|
||||||
"listen": "test",
|
|
||||||
"script": {
|
|
||||||
"exec": [
|
|
||||||
"const jsonData = pm.response.json();",
|
|
||||||
"pm.collectionVariables.set('NOTE_ID', jsonData.id)",
|
|
||||||
""
|
|
||||||
],
|
|
||||||
"type": "text/javascript"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"request": {
|
|
||||||
"method": "POST",
|
|
||||||
"header": [],
|
|
||||||
"body": {
|
|
||||||
"mode": "raw",
|
|
||||||
"raw": "{\n \"contents\": \"Some encrypted content\",\n \"views\": 1,\n \"meta\": \"{\\\"type\\\":\\\"text\\\"}\"\n}",
|
|
||||||
"options": {
|
|
||||||
"raw": {
|
|
||||||
"language": "json"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"url": {
|
|
||||||
"raw": "{{BASE}}/notes/",
|
|
||||||
"host": ["{{BASE}}"],
|
|
||||||
"path": ["notes", ""]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"response": [
|
|
||||||
{
|
|
||||||
"name": "Simple",
|
|
||||||
"originalRequest": {
|
|
||||||
"method": "POST",
|
|
||||||
"header": [],
|
|
||||||
"body": {
|
|
||||||
"mode": "raw",
|
|
||||||
"raw": "{\n \"contents\": \"Some encrypted content\",\n \"views\": 1,\n \"meta\": \"{\\\"type\\\":\\\"text\\\"}\"\n}",
|
|
||||||
"options": {
|
|
||||||
"raw": {
|
|
||||||
"language": "json"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"url": {
|
|
||||||
"raw": "{{BASE}}/notes/",
|
|
||||||
"host": ["{{BASE}}"],
|
|
||||||
"path": ["notes", ""]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"status": "OK",
|
|
||||||
"code": 200,
|
|
||||||
"_postman_previewlanguage": "json",
|
|
||||||
"header": [
|
|
||||||
{
|
|
||||||
"key": "transfer-encoding",
|
|
||||||
"value": "chunked"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "connection",
|
|
||||||
"value": "close"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "content-encoding",
|
|
||||||
"value": "gzip"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "content-type",
|
|
||||||
"value": "application/json"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "vary",
|
|
||||||
"value": "accept-encoding"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "date",
|
|
||||||
"value": "Tue, 23 May 2023 05:31:54 GMT"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"cookie": [],
|
|
||||||
"body": "{\n \"id\": \"1QeEWDQbQY9dOo8cDDQjykaEjouqugTR6A78sjgn4VMv\"\n}"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "5 Minutes",
|
|
||||||
"originalRequest": {
|
|
||||||
"method": "POST",
|
|
||||||
"header": [],
|
|
||||||
"body": {
|
|
||||||
"mode": "raw",
|
|
||||||
"raw": "{\n \"contents\": \"Some encrypted content\",\n \"expiration\": 5,\n \"meta\": \"{\\\"type\\\":\\\"text\\\"}\"\n}",
|
|
||||||
"options": {
|
|
||||||
"raw": {
|
|
||||||
"language": "json"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"url": {
|
|
||||||
"raw": "{{BASE}}/notes/",
|
|
||||||
"host": ["{{BASE}}"],
|
|
||||||
"path": ["notes", ""]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"status": "OK",
|
|
||||||
"code": 200,
|
|
||||||
"_postman_previewlanguage": "json",
|
|
||||||
"header": [
|
|
||||||
{
|
|
||||||
"key": "transfer-encoding",
|
|
||||||
"value": "chunked"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "connection",
|
|
||||||
"value": "close"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "content-encoding",
|
|
||||||
"value": "gzip"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "content-type",
|
|
||||||
"value": "application/json"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "vary",
|
|
||||||
"value": "accept-encoding"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "date",
|
|
||||||
"value": "Tue, 23 May 2023 05:31:54 GMT"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"cookie": [],
|
|
||||||
"body": "{\n \"id\": \"1QeEWDQbQY9dOo8cDDQjykaEjouqugTR6A78sjgn4VMv\"\n}"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "3 Views",
|
|
||||||
"originalRequest": {
|
|
||||||
"method": "POST",
|
|
||||||
"header": [],
|
|
||||||
"body": {
|
|
||||||
"mode": "raw",
|
|
||||||
"raw": "{\n \"contents\": \"Some encrypted content\",\n \"views\": 3,\n \"meta\": \"{\\\"type\\\":\\\"text\\\"}\"\n}",
|
|
||||||
"options": {
|
|
||||||
"raw": {
|
|
||||||
"language": "json"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"url": {
|
|
||||||
"raw": "{{BASE}}/notes/",
|
|
||||||
"host": ["{{BASE}}"],
|
|
||||||
"path": ["notes", ""]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"status": "OK",
|
|
||||||
"code": 200,
|
|
||||||
"_postman_previewlanguage": "json",
|
|
||||||
"header": [
|
|
||||||
{
|
|
||||||
"key": "transfer-encoding",
|
|
||||||
"value": "chunked"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "connection",
|
|
||||||
"value": "close"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "content-encoding",
|
|
||||||
"value": "gzip"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "content-type",
|
|
||||||
"value": "application/json"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "vary",
|
|
||||||
"value": "accept-encoding"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "date",
|
|
||||||
"value": "Tue, 23 May 2023 05:31:54 GMT"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"cookie": [],
|
|
||||||
"body": "{\n \"id\": \"1QeEWDQbQY9dOo8cDDQjykaEjouqugTR6A78sjgn4VMv\"\n}"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Read",
|
|
||||||
"request": {
|
|
||||||
"method": "DELETE",
|
|
||||||
"header": [],
|
|
||||||
"url": {
|
|
||||||
"raw": "{{BASE}}/notes/:id",
|
|
||||||
"host": ["{{BASE}}"],
|
|
||||||
"path": ["notes", ":id"],
|
|
||||||
"variable": [
|
|
||||||
{
|
|
||||||
"key": "id",
|
|
||||||
"value": "{{NOTE_ID}}"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"description": "This endpoint gets the actual contents of a note. It's a `DELETE` endpoint, es it decreases the `view` counter, and deletes the note if `0` is reached."
|
|
||||||
},
|
|
||||||
"response": [
|
|
||||||
{
|
|
||||||
"name": "200",
|
|
||||||
"originalRequest": {
|
|
||||||
"method": "DELETE",
|
|
||||||
"header": [],
|
|
||||||
"url": {
|
|
||||||
"raw": "{{BASE}}/notes/:id",
|
|
||||||
"host": ["{{BASE}}"],
|
|
||||||
"path": ["notes", ":id"],
|
|
||||||
"variable": [
|
|
||||||
{
|
|
||||||
"key": "id",
|
|
||||||
"value": "{{NOTE_ID}}"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"status": "OK",
|
|
||||||
"code": 200,
|
|
||||||
"_postman_previewlanguage": "json",
|
|
||||||
"header": [
|
|
||||||
{
|
|
||||||
"key": "transfer-encoding",
|
|
||||||
"value": "chunked"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "connection",
|
|
||||||
"value": "close"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "content-type",
|
|
||||||
"value": "application/json"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "vary",
|
|
||||||
"value": "accept-encoding"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "content-encoding",
|
|
||||||
"value": "gzip"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "date",
|
|
||||||
"value": "Tue, 23 May 2023 05:59:07 GMT"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"cookie": [],
|
|
||||||
"body": "{\n \"meta\": \"{\\\"type\\\":\\\"text\\\"}\",\n \"contents\": \"Some encrypted content\"\n}"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "404",
|
|
||||||
"originalRequest": {
|
|
||||||
"method": "DELETE",
|
|
||||||
"header": [],
|
|
||||||
"url": {
|
|
||||||
"raw": "{{BASE}}/notes/:id",
|
|
||||||
"host": ["{{BASE}}"],
|
|
||||||
"path": ["notes", ":id"],
|
|
||||||
"variable": [
|
|
||||||
{
|
|
||||||
"key": "id",
|
|
||||||
"value": "{{NOTE_ID}}"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"status": "Not Found",
|
|
||||||
"code": 404,
|
|
||||||
"_postman_previewlanguage": "plain",
|
|
||||||
"header": [
|
|
||||||
{
|
|
||||||
"key": "transfer-encoding",
|
|
||||||
"value": "chunked"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "connection",
|
|
||||||
"value": "close"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "vary",
|
|
||||||
"value": "accept-encoding"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "content-encoding",
|
|
||||||
"value": "gzip"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "date",
|
|
||||||
"value": "Tue, 23 May 2023 05:59:15 GMT"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"cookie": [],
|
|
||||||
"body": ""
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Status",
|
|
||||||
"item": [
|
|
||||||
{
|
|
||||||
"name": "Get server status",
|
|
||||||
"request": {
|
|
||||||
"method": "GET",
|
|
||||||
"header": [],
|
|
||||||
"url": {
|
|
||||||
"raw": "{{BASE}}/status/",
|
|
||||||
"host": ["{{BASE}}"],
|
|
||||||
"path": ["status", ""]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"response": [
|
|
||||||
{
|
|
||||||
"name": "200",
|
|
||||||
"originalRequest": {
|
|
||||||
"method": "GET",
|
|
||||||
"header": [],
|
|
||||||
"url": {
|
|
||||||
"raw": "{{BASE}}/status/",
|
|
||||||
"host": ["{{BASE}}"],
|
|
||||||
"path": ["status", ""]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"status": "OK",
|
|
||||||
"code": 200,
|
|
||||||
"_postman_previewlanguage": "json",
|
|
||||||
"header": [
|
|
||||||
{
|
|
||||||
"key": "transfer-encoding",
|
|
||||||
"value": "chunked"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "connection",
|
|
||||||
"value": "close"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "content-encoding",
|
|
||||||
"value": "gzip"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "vary",
|
|
||||||
"value": "accept-encoding"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "content-type",
|
|
||||||
"value": "application/json"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "date",
|
|
||||||
"value": "Tue, 23 May 2023 05:56:45 GMT"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"cookie": [],
|
|
||||||
"body": "{\n \"version\": \"2.3.0-beta.4\",\n \"max_size\": 10485760,\n \"max_views\": 100,\n \"max_expiration\": 360,\n \"allow_advanced\": true,\n \"theme_image\": \"\",\n \"theme_text\": \"\",\n \"theme_page_title\": \"\",\n \"theme_favicon\": \"\"\n}"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Health Check",
|
|
||||||
"request": {
|
|
||||||
"method": "GET",
|
|
||||||
"header": [],
|
|
||||||
"url": {
|
|
||||||
"raw": "{{BASE}}/live/",
|
|
||||||
"host": ["{{BASE}}"],
|
|
||||||
"path": ["live", ""]
|
|
||||||
},
|
|
||||||
"description": "Return `200` for healthy service. `503` if service is unavailable."
|
|
||||||
},
|
|
||||||
"response": [
|
|
||||||
{
|
|
||||||
"name": "Healthy",
|
|
||||||
"originalRequest": {
|
|
||||||
"method": "GET",
|
|
||||||
"header": [],
|
|
||||||
"url": {
|
|
||||||
"raw": "{{BASE}}/live/",
|
|
||||||
"host": ["{{BASE}}"],
|
|
||||||
"path": ["live", ""]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"status": "OK",
|
|
||||||
"code": 200,
|
|
||||||
"_postman_previewlanguage": "plain",
|
|
||||||
"header": [
|
|
||||||
{
|
|
||||||
"key": "transfer-encoding",
|
|
||||||
"value": "chunked"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "vary",
|
|
||||||
"value": "accept-encoding"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "content-encoding",
|
|
||||||
"value": "gzip"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "date",
|
|
||||||
"value": "Thu, 22 Jun 2023 20:17:58 GMT"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"cookie": [],
|
|
||||||
"body": null
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Service Unavilable",
|
|
||||||
"originalRequest": {
|
|
||||||
"method": "GET",
|
|
||||||
"header": [],
|
|
||||||
"url": {
|
|
||||||
"raw": "{{BASE}}/live/",
|
|
||||||
"host": ["{{BASE}}"],
|
|
||||||
"path": ["live", ""]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"status": "Service Unavailable",
|
|
||||||
"code": 503,
|
|
||||||
"_postman_previewlanguage": "plain",
|
|
||||||
"header": [
|
|
||||||
{
|
|
||||||
"key": "transfer-encoding",
|
|
||||||
"value": "chunked"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "content-encoding",
|
|
||||||
"value": "gzip"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "vary",
|
|
||||||
"value": "accept-encoding"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "date",
|
|
||||||
"value": "Thu, 22 Jun 2023 20:18:55 GMT"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"cookie": [],
|
|
||||||
"body": null
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"event": [
|
|
||||||
{
|
|
||||||
"listen": "prerequest",
|
|
||||||
"script": {
|
|
||||||
"type": "text/javascript",
|
|
||||||
"exec": [""]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"listen": "test",
|
|
||||||
"script": {
|
|
||||||
"type": "text/javascript",
|
|
||||||
"exec": [""]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"variable": [
|
|
||||||
{
|
|
||||||
"key": "BASE",
|
|
||||||
"value": "http://localhost:3000/api",
|
|
||||||
"type": "default"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "NOTE_ID",
|
|
||||||
"value": "",
|
|
||||||
"type": "default"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -55,7 +55,7 @@ There is an [official Raycast extension](https://www.raycast.com/cupcakearmy/cry
|
|||||||
|
|
||||||
each note has a generated <code>id (256bit)</code> and <code>key 256(bit)</code>. The
|
each note has a generated <code>id (256bit)</code> and <code>key 256(bit)</code>. The
|
||||||
<code>id</code>
|
<code>id</code>
|
||||||
is used to save & retrieve the note. the note is then encrypted with aes in gcm mode on the
|
is used to save & retrieve the note. the note is then encrypted with XChaCha20-Poly1305 on the
|
||||||
client side with the <code>key</code> and then sent to the server. data is stored in memory and
|
client side with the <code>key</code> and then sent to the server. data is stored in memory and
|
||||||
never persisted to disk. the server never sees the encryption key and cannot decrypt the contents
|
never persisted to disk. the server never sees the encryption key and cannot decrypt the contents
|
||||||
of the notes even if it tried to.
|
of the notes even if it tried to.
|
||||||
@@ -68,31 +68,32 @@ of the notes even if it tried to.
|
|||||||
|
|
||||||
## Environment Variables
|
## Environment Variables
|
||||||
|
|
||||||
| Variable | Default | Description |
|
| Variable | Default | Description |
|
||||||
| ----------------------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
| ----------------------- | ---------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| `CACHE` | `redis://cache/` | Cache URL (valkey or redis) to connect to. [According to format](https://docs.rs/redis/latest/redis/#connection-parameters) |
|
| `CACHE` | `redis://cache/` | Cache URL (valkey or redis) to connect to. [According to format](https://docs.rs/redis/latest/redis/#connection-parameters) |
|
||||||
| `SIZE_LIMIT` | `1 KiB` | Max size for body. Accepted values according to [byte-unit](https://docs.rs/byte-unit/). <br> `512 MiB` is the maximum allowed. <br> The frontend will show that number including the ~35% encoding overhead. |
|
| `SIZE_LIMIT` | `1 KiB` | Max size for body. Accepted values according to [byte-unit](https://docs.rs/byte-unit/). <br> `512 MiB` is the maximum allowed. <br> Payloads are raw bytes (msgpack + cipher), so the frontend shows the full limit. |
|
||||||
| `MAX_VIEWS` | `100` | Maximal number of views. |
|
| `MAX_VIEWS` | `100` | Maximal number of views. |
|
||||||
| `MAX_EXPIRATION` | `360` | Maximal expiration in minutes. |
|
| `MAX_EXPIRATION` | `360` | Maximal expiration in minutes. |
|
||||||
| `ALLOW_ADVANCED` | `true` | Allow custom configuration. If set to `false` all notes will be one view only. |
|
| `ALLOW_ADVANCED` | `true` | Allow custom configuration. If set to `false` all notes will be one view only. |
|
||||||
| `ALLOW_FILES` | `true` | Allow uploading files. If set to `false`, users will only be allowed to create text notes. |
|
| `ALLOW_FILES` | `true` | Allow uploading files. If set to `false`, users will only be allowed to create text notes. |
|
||||||
| `ID_LENGTH` | `32` | Set the size of the note `id` in bytes. By default this is `32` bytes. This is useful for reducing link size. _This setting does not affect encryption strength_. |
|
| `ID_LENGTH` | `32` | Set the size of the note `id` in bytes. By default this is `32` bytes. This is useful for reducing link size. _This setting does not affect encryption strength_. |
|
||||||
| `CACHE_PREFIX` | `""` | Optional prefix for all cache keys. Useful when sharing a cache instance with other apps via ACL namespaces. |
|
| `CACHE_PREFIX` | `""` | Optional prefix for all cache keys. Useful when sharing a cache instance with other apps via ACL namespaces. |
|
||||||
| `VERBOSITY` | `warn` | Verbosity level for the backend. [Possible values](https://docs.rs/env_logger/latest/env_logger/#enabling-logging) are: `error`, `warn`, `info`, `debug`, `trace` |
|
| `EXTRA_SIZE_LIMIT` | `512` | Maximum size in bytes of the opaque `extra` payload (e.g. key derivation params) stored on the note metadata. |
|
||||||
| `THEME_IMAGE` | `""` | Custom image for replacing the logo. Must be publicly reachable |
|
| `VERBOSITY` | `warn` | Verbosity level for the backend. [Possible values](https://docs.rs/env_logger/latest/env_logger/#enabling-logging) are: `error`, `warn`, `info`, `debug`, `trace` |
|
||||||
| `THEME_TEXT` | `""` | Custom text for replacing the description below the logo |
|
| `THEME_IMAGE` | `""` | Custom image for replacing the logo. Must be publicly reachable |
|
||||||
| `THEME_PAGE_TITLE` | `""` | Custom text the page title |
|
| `THEME_TEXT` | `""` | Custom text for replacing the description below the logo |
|
||||||
| `THEME_FAVICON` | `""` | Custom url for the favicon. Must be publicly reachable |
|
| `THEME_PAGE_TITLE` | `""` | Custom text the page title |
|
||||||
| `THEME_NEW_NOTE_NOTICE` | `true` | Show the message about how notes are stored in the memory and may be evicted after creating a new note. Defaults to `true`. |
|
| `THEME_FAVICON` | `""` | Custom url for the favicon. Must be publicly reachable |
|
||||||
| `THEME_HOME_LINK` | `true` | Show the `/home` link in the footer. Defaults to `true`. |
|
| `THEME_NEW_NOTE_NOTICE` | `true` | Show the message about how notes are stored in the memory and may be evicted after creating a new note. Defaults to `true`. |
|
||||||
| `IMPRINT_URL` | `""` | Custom url for an Imprint hosted somewhere else. Must be publicly reachable. Takes precedence above `IMPRINT_HTML`. |
|
| `THEME_HOME_LINK` | `true` | Show the `/home` link in the footer. Defaults to `true`. |
|
||||||
| `IMPRINT_HTML` | `""` | Alternative to `IMPRINT_URL`, this can be used to specify the HTML code to show on `/imprint`. Only `IMPRINT_HTML` or `IMPRINT_URL` should be specified, not both. |
|
| `IMPRINT_URL` | `""` | Custom url for an Imprint hosted somewhere else. Must be publicly reachable. Takes precedence above `IMPRINT_HTML`. |
|
||||||
|
| `IMPRINT_HTML` | `""` | Alternative to `IMPRINT_URL`, this can be used to specify the HTML code to show on `/imprint`. Only `IMPRINT_HTML` or `IMPRINT_URL` should be specified, not both. |
|
||||||
|
|
||||||
## Deployment
|
## Deployment
|
||||||
|
|
||||||
> ℹ️ `https` is required otherwise browsers will not support the cryptographic functions.
|
> ℹ️ `https` is required otherwise browsers will not support the cryptographic functions.
|
||||||
|
|
||||||
> ℹ️ There is a health endpoint available at `/api/health/`. It returns either 200 or 503.
|
> ℹ️ There is a health endpoint available at `/healthz`. It returns either 200 or 503.
|
||||||
|
|
||||||
### Docker
|
### Docker
|
||||||
|
|
||||||
@@ -101,8 +102,6 @@ Docker is the easiest way. There is the [official image here](https://hub.docker
|
|||||||
```yaml
|
```yaml
|
||||||
# docker-compose.yml
|
# docker-compose.yml
|
||||||
|
|
||||||
version: "3.8"
|
|
||||||
|
|
||||||
services:
|
services:
|
||||||
cache:
|
cache:
|
||||||
image: valkey/valkey:7-alpine
|
image: valkey/valkey:7-alpine
|
||||||
|
|||||||
+29
-25
@@ -48,7 +48,7 @@ Puedes revisar la documentación sobre el CLI en este [readme](./packages/cli/RE
|
|||||||
|
|
||||||
Se genera una <code>id (256bit)</code> y una <code>llave 256(bit)</code> para cada nota. La
|
Se genera una <code>id (256bit)</code> y una <code>llave 256(bit)</code> para cada nota. La
|
||||||
<code>id</code>
|
<code>id</code>
|
||||||
se usa para guardar y recuperar la nota. Después la nota es encriptada con la <code>llave</code> y con aes en modo gcm del lado del cliente y por último se envía al servidor. La información es almacenada en memoria y nunca persiste en el disco. El servidor nunca ve la llave de encriptación por lo que no puede desencriptar el contenido de las notas aunque lo intentara.
|
se usa para guardar y recuperar la nota. Después la nota es encriptada con XChaCha20-Poly1305 del lado del cliente y por último se envía al servidor. La información es almacenada en memoria y nunca persiste en el disco. El servidor nunca ve la llave de encriptación por lo que no puede desencriptar el contenido de las notas aunque lo intentara.
|
||||||
|
|
||||||
## Capturas de pantalla
|
## Capturas de pantalla
|
||||||
|
|
||||||
@@ -56,26 +56,32 @@ se usa para guardar y recuperar la nota. Después la nota es encriptada con la <
|
|||||||
|
|
||||||
## Variables de entorno
|
## Variables de entorno
|
||||||
|
|
||||||
| Variable | Default | Descripción |
|
| Variable | Default | Descripción |
|
||||||
| ------------------ | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
| ----------------------- | ---------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| `REDIS` | `redis://redis/` | Redis URL a la que conectarse. [Según el formato](https://docs.rs/redis/latest/redis/#connection-parameters) |
|
| `CACHE` | `redis://cache/` | URL de caché (valkey o redis) a la que conectarse. [Según el formato](https://docs.rs/redis/latest/redis/#connection-parameters) |
|
||||||
| `SIZE_LIMIT` | `1 KiB` | Tamaño máximo. Valores aceptados según la [unidad byte](https://docs.rs/byte-unit/). <br> `512 MiB` es el máximo permitido. <br> El frontend mostrará ese número, incluyendo el ~35% de sobrecarga de codificación. |
|
| `SIZE_LIMIT` | `1 KiB` | Tamaño máximo del cuerpo. Valores aceptados según [byte-unit](https://docs.rs/byte-unit/). <br> `512 MiB` es el máximo permitido. <br> Los payloads son bytes crudos (msgpack + cifrado), por lo que el frontend muestra el límite completo. |
|
||||||
| `MAX_VIEWS` | `100` | Número máximo de vistas. |
|
| `MAX_VIEWS` | `100` | Número máximo de vistas. |
|
||||||
| `MAX_EXPIRATION` | `360` | Tiempo máximo de expiración en minutos. |
|
| `MAX_EXPIRATION` | `360` | Tiempo máximo de expiración en minutos. |
|
||||||
| `ALLOW_ADVANCED` | `true` | Permitir configuración personalizada. Si se establece en `false` todas las notas serán de una sola vista. |
|
| `ALLOW_ADVANCED` | `true` | Permitir configuración personalizada. Si se establece en `false` todas las notas serán de una sola vista. |
|
||||||
| `ID_LENGTH` | `32` | Establece el tamaño en bytes de la `id` de la nota. Por defecto es de `32` bytes. Esto es útil para reducir el tamaño del link. _Esta configuración no afecta el nivel de encriptación_. |
|
| `ALLOW_FILES` | `true` | Permitir subir archivos. Si es `false`, los usuarios solo podrán crear notas de texto. |
|
||||||
| `VERBOSITY` | `warn` | Nivel de verbosidad del backend. [Posibles valores](https://docs.rs/env_logger/latest/env_logger/#enabling-logging): `error`, `warn`, `info`, `debug`, `trace` |
|
| `ID_LENGTH` | `32` | Establece el tamaño en bytes de la `id` de la nota. Por defecto es de `32` bytes. Útil para reducir el tamaño del link. _No afecta el nivel de encriptación_. |
|
||||||
| `THEME_IMAGE` | `""` | Imagen personalizada para reemplazar el logo. Debe ser accesible públicamente. |
|
| `CACHE_PREFIX` | `""` | Prefijo opcional para las claves de caché. Útil al compartir una instancia con otras apps vía namespaces ACL. |
|
||||||
| `THEME_TEXT` | `""` | Texto personalizado para reemplazar la descripción bajo el logo. |
|
| `EXTRA_SIZE_LIMIT` | `512` | Tamaño máximo en bytes del payload `extra` opaco (p. ej. parámetros de derivación de clave) guardado en los metadatos de la nota. |
|
||||||
| `THEME_PAGE_TITLE` | `""` | Texto personalizado para el título |
|
| `VERBOSITY` | `warn` | Nivel de verbosidad del backend. [Posibles valores](https://docs.rs/env_logger/latest/env_logger/#enabling-logging): `error`, `warn`, `info`, `debug`, `trace` |
|
||||||
| `THEME_FAVICON` | `""` | Url personalizada para el favicon. Debe ser accesible públicamente. |
|
| `THEME_IMAGE` | `""` | Imagen personalizada para reemplazar el logo. Debe ser accesible públicamente. |
|
||||||
| `THEME_HOME_LINK` | `true` | Mostrar el enlace `/home` en el pie de página. El valor predeterminado es `true`. |
|
| `THEME_TEXT` | `""` | Texto personalizado para reemplazar la descripción bajo el logo. |
|
||||||
|
| `THEME_PAGE_TITLE` | `""` | Texto personalizado para el título. |
|
||||||
|
| `THEME_FAVICON` | `""` | Url personalizada para el favicon. Debe ser accesible públicamente. |
|
||||||
|
| `THEME_NEW_NOTE_NOTICE` | `true` | Mostrar el mensaje sobre cómo se almacenan las notas en memoria (pueden ser expulsadas) al crear una nueva nota. |
|
||||||
|
| `THEME_HOME_LINK` | `true` | Mostrar el enlace `/home` en el pie de página. El valor predeterminado es `true`. |
|
||||||
|
| `IMPRINT_URL` | `""` | URL personalizada para un imprint alojado en otro sitio. Debe ser accesible públicamente. Tiene prioridad sobre `IMPRINT_HTML`. |
|
||||||
|
| `IMPRINT_HTML` | `""` | Alternativa a `IMPRINT_URL` para especificar el HTML a mostrar en `/imprint`. Usa solo `IMPRINT_HTML` o `IMPRINT_URL`, no ambos. |
|
||||||
|
|
||||||
## Despliegue
|
## Despliegue
|
||||||
|
|
||||||
> ℹ️ Se requiere `https` de lo contrario el navegador no soportará las funciones de encriptación.
|
> ℹ️ Se requiere `https` de lo contrario el navegador no soportará las funciones de encriptación.
|
||||||
|
|
||||||
> ℹ️ Hay un endpoint para verificar el estado, lo encontramos en `/api/health/`. Regresa un código 200 o 503.
|
> ℹ️ Hay un endpoint para verificar el estado, lo encontramos en `/healthz`. Regresa un código 200 o 503.
|
||||||
|
|
||||||
### Docker
|
### Docker
|
||||||
|
|
||||||
@@ -84,16 +90,14 @@ Docker es la manera más fácil. Aquí encontramos [la imagen oficial](https://h
|
|||||||
```yaml
|
```yaml
|
||||||
# docker-compose.yml
|
# docker-compose.yml
|
||||||
|
|
||||||
version: "3.8"
|
|
||||||
|
|
||||||
services:
|
services:
|
||||||
redis:
|
cache:
|
||||||
image: redis:7-alpine
|
image: valkey/valkey:7-alpine
|
||||||
# This is required to stay in RAM only.
|
# This is required to stay in RAM only.
|
||||||
command: redis-server --save "" --appendonly no
|
command: valkey-server --save "" --appendonly no
|
||||||
# Set a size limit. See link below on how to customise.
|
# Set a size limit. See link below on how to customise.
|
||||||
# https://redis.io/docs/latest/operate/rs/databases/memory-performance/eviction-policy/
|
# https://valkey.io/docs/latest/operate/rs/databases/memory-performance/eviction-policy/
|
||||||
# --maxmemory 1gb --maxmemory-policy allkeys-lrulpine
|
# --maxmemory 1g --maxmemory-policy allkeys-lrulpine
|
||||||
# This prevents the creation of an anonymous volume.
|
# This prevents the creation of an anonymous volume.
|
||||||
tmpfs:
|
tmpfs:
|
||||||
- /data
|
- /data
|
||||||
@@ -101,7 +105,7 @@ services:
|
|||||||
app:
|
app:
|
||||||
image: cupcakearmy/cryptgeon:latest
|
image: cupcakearmy/cryptgeon:latest
|
||||||
depends_on:
|
depends_on:
|
||||||
- redis
|
- cache
|
||||||
environment:
|
environment:
|
||||||
# Size limit for a single note.
|
# Size limit for a single note.
|
||||||
SIZE_LIMIT: 4 MiB
|
SIZE_LIMIT: 4 MiB
|
||||||
@@ -110,7 +114,7 @@ services:
|
|||||||
|
|
||||||
# Optional health checks
|
# Optional health checks
|
||||||
# healthcheck:
|
# healthcheck:
|
||||||
# test: ["CMD", "curl", "--fail", "http://127.0.0.1:8000/api/live/"]
|
# test: ["CMD", "curl", "--fail", "http://127.0.0.1:8000/healthz"]
|
||||||
# interval: 1m
|
# interval: 1m
|
||||||
# timeout: 3s
|
# timeout: 3s
|
||||||
# retries: 2
|
# retries: 2
|
||||||
|
|||||||
+31
-24
@@ -36,7 +36,7 @@ _加密鸽_ 是一个受 [_PrivNote_](https://privnote.com)项目启发的安全
|
|||||||
|
|
||||||
加密鸽会为每条笔记都生成一个独立的 <code>id (256bit)</code> 和 <code>key 256(bit)</code>。
|
加密鸽会为每条笔记都生成一个独立的 <code>id (256bit)</code> 和 <code>key 256(bit)</code>。
|
||||||
|
|
||||||
其中<code>id</code>用于保存和提取密信, 在这之后这封密信将会被客户端使用 AES 算法的 GCM 模式和`key`进行加密然后发送至服务器,数据将会保存在服务器的内存中且永远不会被持久化到硬盘上,服务端永远不会得到密钥并且无法解读密信的内容。
|
其中<code>id</code>用于保存和提取密信, 在这之后这封密信将会被客户端使用 XChaCha20-Poly1305 加密算法和`key`进行加密然后发送至服务器,数据将会保存在服务器的内存中且永远不会被持久化到硬盘上,服务端永远不会得到密钥并且无法解读密信的内容。
|
||||||
|
|
||||||
## 屏幕截图
|
## 屏幕截图
|
||||||
|
|
||||||
@@ -44,16 +44,26 @@ _加密鸽_ 是一个受 [_PrivNote_](https://privnote.com)项目启发的安全
|
|||||||
|
|
||||||
## 环境变量
|
## 环境变量
|
||||||
|
|
||||||
| 变量名称 | 默认值 | 描述 |
|
| 变量名称 | 默认值 | 描述 |
|
||||||
| ---------------- | ---------------- | --------------------------------------------------------------------------------- |
|
| ----------------------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||||
| `REDIS` | `redis://redis/` | Redis 连接 URL。 |
|
| `CACHE` | `redis://cache/` | 缓存(valkey 或 redis)连接 URL。[连接参数](https://docs.rs/redis/latest/redis/#connection-parameters) |
|
||||||
| `SIZE_LIMIT` | `1 KiB` | 最大请求体(body)限制。有关支持的数值请查看 [字节单位](https://docs.rs/byte-unit/) |
|
| `SIZE_LIMIT` | `1 KiB` | 最大请求体(body)限制。可通过 [字节单位](https://docs.rs/byte-unit/) 查看支持的值。负载是原始字节(msgpack + 加密),因此前端显示完整限制。 |
|
||||||
| `MAX_VIEWS` | `100` | 密信最多查看次数限制 |
|
| `MAX_VIEWS` | `100` | 密信最多查看次数限制。 |
|
||||||
| `MAX_EXPIRATION` | `360` | 密信最长过期时间限制(分钟) |
|
| `MAX_EXPIRATION` | `360` | 密信最长过期时间限制(分钟)。 |
|
||||||
| `ALLOW_ADVANCED` | `true` | 是否允许自定义设置,该项如果设为`false`,则不会显示自定义设置模块 |
|
| `ALLOW_ADVANCED` | `true` | 是否允许自定义设置,该项如果设为`false`,则不会显示自定义设置模块。 |
|
||||||
| `THEME_IMAGE` | `""` | 自定义 Logo 图片,你在这里填写的的图片链接必须是可以公开访问的。 |
|
| `ALLOW_FILES` | `true` | 是否允许上传文件。为 `false` 时用户只能创建文本密信。 |
|
||||||
| `THEME_TEXT` | `""` | 自定义在 Logo 下方的文本。 |
|
| `ID_LENGTH` | `32` | 设置密信 `id` 的字节大小。默认 `32` 字节,可用于缩短链接长度。_不影响加密强度_。 |
|
||||||
| `THEME_HOME_LINK` | `true` | 是否在页脚显示 `/home` 链接。默认为 `true`。 |
|
| `CACHE_PREFIX` | `""` | 缓存键可选前缀。与其它应用通过 ACL namespace 共享缓存实例时有用。 |
|
||||||
|
| `EXTRA_SIZE_LIMIT` | `512` | 不透明 `extra` 负载(如密钥派生参数)的最大字节数,存于密信元数据。 |
|
||||||
|
| `VERBOSITY` | `warn` | 后端日志级别。可能值见 [env_logger](https://docs.rs/env_logger/latest/env_logger/#enabling-logging)。 |
|
||||||
|
| `THEME_IMAGE` | `""` | 自定义 Logo 图片,需可公开访问。 |
|
||||||
|
| `THEME_TEXT` | `""` | 自定义在 Logo 下方的文本。 |
|
||||||
|
| `THEME_PAGE_TITLE` | `""` | 自定义页面标题。 |
|
||||||
|
| `THEME_FAVICON` | `""` | 自定义 favicon 地址,需可公开访问。 |
|
||||||
|
| `THEME_NEW_NOTE_NOTICE` | `true` | 创建新笔记后显示“笔记存于内存可能被清除”的提示。 |
|
||||||
|
| `THEME_HOME_LINK` | `true` | 是否在页脚显示 `/home` 链接。默认为 `true`。 |
|
||||||
|
| `IMPRINT_URL` | `""` | 托管在其它位置的印页 URL,需可公开访问。优先于 `IMPRINT_HTML`。 |
|
||||||
|
| `IMPRINT_HTML` | `""` | `IMPRINT_URL` 的替代:指定 `/imprint` 展示的 HTML。`IMPRINT_HTML` 与 `IMPRINT_URL` 只应指定其一。 | |
|
||||||
|
|
||||||
## 部署
|
## 部署
|
||||||
|
|
||||||
@@ -67,15 +77,14 @@ Docker 是最简单的部署方式。这里是[官方镜像的地址](https://hu
|
|||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
# docker-compose.yml
|
# docker-compose.yml
|
||||||
version: "3.8"
|
|
||||||
|
|
||||||
services:
|
services:
|
||||||
redis:
|
cache:
|
||||||
image: redis:7-alpine
|
image: valkey/valkey:7-alpine
|
||||||
# This is required to stay in RAM only.
|
# This is required to stay in RAM only.
|
||||||
command: redis-server --save "" --appendonly no
|
command: valkey-server --save "" --appendonly no
|
||||||
# Set a size limit. See link below on how to customise.
|
# Set a size limit. See link below on how to customise.
|
||||||
# https://redis.io/docs/latest/operate/rs/databases/memory-performance/eviction-policy/
|
# https://valkey.io/docs/latest/operate/rs/databases/memory-performance/eviction-policy/
|
||||||
# --maxmemory 1gb --maxmemory-policy allkeys-lrulpine
|
# --maxmemory 1gb --maxmemory-policy allkeys-lrulpine
|
||||||
# This prevents the creation of an anonymous volume.
|
# This prevents the creation of an anonymous volume.
|
||||||
tmpfs:
|
tmpfs:
|
||||||
@@ -84,7 +93,7 @@ services:
|
|||||||
app:
|
app:
|
||||||
image: cupcakearmy/cryptgeon:latest
|
image: cupcakearmy/cryptgeon:latest
|
||||||
depends_on:
|
depends_on:
|
||||||
- redis
|
- cache
|
||||||
environment:
|
environment:
|
||||||
SIZE_LIMIT: 4 MiB
|
SIZE_LIMIT: 4 MiB
|
||||||
ports:
|
ports:
|
||||||
@@ -105,19 +114,17 @@ services:
|
|||||||
- 域名 `example.org`
|
- 域名 `example.org`
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
version: "3.8"
|
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
proxy:
|
proxy:
|
||||||
external: true
|
external: true
|
||||||
|
|
||||||
services:
|
services:
|
||||||
redis:
|
cache:
|
||||||
image: redis:7-alpine
|
image: valkey/valkey:7-alpine
|
||||||
# This is required to stay in RAM only.
|
# This is required to stay in RAM only.
|
||||||
command: redis-server --save "" --appendonly no
|
command: valkey-server --save "" --appendonly no
|
||||||
# Set a size limit. See link below on how to customise.
|
# Set a size limit. See link below on how to customise.
|
||||||
# https://redis.io/docs/latest/operate/rs/databases/memory-performance/eviction-policy/
|
# https://valkey.io/docs/latest/operate/rs/databases/memory-performance/eviction-policy/
|
||||||
# --maxmemory 1gb --maxmemory-policy allkeys-lrulpine
|
# --maxmemory 1gb --maxmemory-policy allkeys-lrulpine
|
||||||
# This prevents the creation of an anonymous volume.
|
# This prevents the creation of an anonymous volume.
|
||||||
tmpfs:
|
tmpfs:
|
||||||
@@ -127,7 +134,7 @@ services:
|
|||||||
image: cupcakearmy/cryptgeon:latest
|
image: cupcakearmy/cryptgeon:latest
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
depends_on:
|
depends_on:
|
||||||
- redis
|
- cache
|
||||||
networks:
|
networks:
|
||||||
- default
|
- default
|
||||||
- proxy
|
- proxy
|
||||||
|
|||||||
@@ -0,0 +1,77 @@
|
|||||||
|
# Roadmap
|
||||||
|
|
||||||
|
## Todo
|
||||||
|
|
||||||
|
- Localize i18n readmes: `README_ES.md`, `README_zh-CN.md` still reference `REDIS` / `/api/v1`-era endpoint names — align to `CACHE` / `/healthz` (also sweep `CONTRIBUTING.md`, `examples/*`, postman collection).
|
||||||
|
- Add remaining shared tooling to the pnpm catalog (`vite`, `tsdown`).
|
||||||
|
- Move formatting, linting and type-checking + git hooks onto `vite-plus` (oxlint, oxfmt, vitest).
|
||||||
|
- Re-add CSP (`Content-Security-Policy`) wired into the axum router (was in `csp.rs`, removed as unused).
|
||||||
|
|
||||||
|
## Unified payload (drop the text/file union)
|
||||||
|
|
||||||
|
> Follow-up iteration of the inner payload, parked as `v3.1` (not part of core v3).
|
||||||
|
|
||||||
|
Everything becomes a **file**. Text is just a `FileDTO` with `inline: true`. The `{ type: "text" } | { type: "files" }` union is removed.
|
||||||
|
|
||||||
|
```
|
||||||
|
# Inner layer (encrypted, client-only)
|
||||||
|
{ files: [
|
||||||
|
{ name: string, mime: string, size: number, data: bytes, inline?: boolean }
|
||||||
|
] }
|
||||||
|
```
|
||||||
|
|
||||||
|
- `FileDTO` gains `inline?: boolean` (default `false`).
|
||||||
|
- `inline: true` = the file was authored inline at compose time (e.g. an empty text file the user typed into). Purely a **client/UI hint** — rides inside the encrypted inner payload, the server never sees it.
|
||||||
|
- `inline: true` files render as an **editable text editor**; the rest render as binary file cards (upload/download).
|
||||||
|
- Default composer state = one empty `inline:true` text file the user edits. No `isFile` toggle.
|
||||||
|
|
||||||
|
### Impact by area
|
||||||
|
|
||||||
|
- **Server / wire protocol / `api.ts`**: unchanged. Still an opaque encrypted `data` blob in the outer msgpack envelope.
|
||||||
|
- **Shared codec**: `NoteContent` becomes `{ files: FileDTO[] }`; drop the union + `switch(type)` in `unpackContent`. `packContent(input: FileDTO[], password?)`. Breaking inner-msgpack schema → ok, pre-release.
|
||||||
|
- **Frontend (`Create.svelte` — biggest)**: one `files: FileDTO[]` model; default empty `inline` text file; editor binds a string, encodes to bytes on submit; add real files via upload (`inline:false`).
|
||||||
|
- **CLI**: `send text "x"` → `files:[{ name:'note.txt', mime:'text/plain', data:utf8, inline:true }]`. `send file a b` → drop `type` union. `open`/download prints text files, saves the rest.
|
||||||
|
- **Tests**: `payload.test.ts` rewritten to `{ files:[...] }`; playwright `switch-file`/`text-field` composer specs collapse + rework.
|
||||||
|
|
||||||
|
### Watches
|
||||||
|
|
||||||
|
- text↔bytes round-trip in the editor (encoding, line-endings);
|
||||||
|
- `size` must be set from the _encoded_ bytes (matches `SIZE_LIMIT` / preview) — recompute after text→bytes;
|
||||||
|
- pasted binary files keep `inline:false`; only inline-authored text is `inline:true`.
|
||||||
|
|
||||||
|
### Payload pipeline
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
subgraph WRITE["CLIENT — encode / compress / encrypt"]
|
||||||
|
A[Text or Files] --> B{password?}
|
||||||
|
B -->|yes| C1[deriveKey password+salt<br>extra=encode salt,N,r,p]
|
||||||
|
B -->|no| C2[generateKey random 32B]
|
||||||
|
C2 --> D[URL fragment hex key]
|
||||||
|
C1 --> E
|
||||||
|
D --> E
|
||||||
|
A --> F[encode inner files]
|
||||||
|
F -->|encode content| G[inner msgpack]
|
||||||
|
G --> H[LZ4 compress]
|
||||||
|
H --> I[XChaCha20 encrypt]
|
||||||
|
I -->|data| J[POST msgpack meta+data]
|
||||||
|
C1 -->|extra| J
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph SERVER["SERVER — agnostic"]
|
||||||
|
J --> K{hash store: views, expiration, extra, data}
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph READ["CLIENT — read"]
|
||||||
|
L[meta/extra from PREVIEW] --> M{extra present?}
|
||||||
|
M -->|yes| N[deriveKey pw+salt]
|
||||||
|
M -->|no| O[key from URL hex fragment]
|
||||||
|
N --> P[DELETE get envelope data]
|
||||||
|
O --> P
|
||||||
|
P --> Q[XChaCha20 decrypt]
|
||||||
|
Q --> R[LZ4 decompress]
|
||||||
|
R --> S[msgpack decode files]
|
||||||
|
S -->|inline:true| T[edit / render text]
|
||||||
|
S -->|inline:false| U[save files]
|
||||||
|
end
|
||||||
|
```
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
# Todo
|
|
||||||
|
|
||||||
- also update readmes in other languages
|
|
||||||
- use catalog install for common deps (typescript, vite, tsdown, etc. ) please suggest.
|
|
||||||
- move formatting, linting, type checking and git hooks to vite-plus (uses oxlint, oxfmt, vitest and git hook dispatcher)
|
|
||||||
- re-add CSP (Content-Security-Policy) into axum router ( (was in csp.rs, removed as unused)
|
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
# v3 Breaking Changes
|
|
||||||
|
|
||||||
A list of changes users and operators need to consider when upgrading from v2 to v3.
|
|
||||||
|
|
||||||
## API
|
|
||||||
|
|
||||||
- All note endpoints moved under `/api/v3/notes/` (was `/api/notes/`)
|
|
||||||
- Status endpoint moved to `/api/v3/status` (was `/api/status`)
|
|
||||||
- All request/response bodies are now **MessagePack** (`Content-Type: application/msgpack`), not JSON
|
|
||||||
- Health check moved to `/healthz` (was `/api/live`)
|
|
||||||
- Notes can now have **both** `views` and `expiration` set simultaneously (previously mutually exclusive)
|
|
||||||
|
|
||||||
## API payload structure
|
|
||||||
|
|
||||||
The wire format changed entirely. v2 used:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{ "contents": "<encrypted string>", "meta": "<stringified JSON>", "views": 5, "expiration": 30 }
|
|
||||||
```
|
|
||||||
|
|
||||||
v3 uses msgpack:
|
|
||||||
|
|
||||||
```
|
|
||||||
{ meta: { views?, expiration?, extra? }, data: <encrypted bytes> }
|
|
||||||
```
|
|
||||||
|
|
||||||
- `meta.extra` holds client-opaque data (e.g. scrypt derivation params), size-limited (default 512 bytes)
|
|
||||||
- `data` is the encrypted blob — the server never inspects its contents
|
|
||||||
- The encrypted inner payload is itself msgpack: `{ type: "text", data: string }` or `{ type: "files", data: [{ name, mime, size, data }] }`
|
|
||||||
|
|
||||||
## Environment variables
|
|
||||||
|
|
||||||
| v2 | v3 |
|
|
||||||
| ------------------- | -------------------- |
|
|
||||||
| `REDIS` | `CACHE` |
|
|
||||||
| `REDIS_PREFIX` | `CACHE_PREFIX` |
|
|
||||||
| _(new)_ | `EXTRA_SIZE_LIMIT` |
|
|
||||||
|
|
||||||
The `CACHE` env var accepts any RESP-compatible URL (valkey or redis).
|
|
||||||
`EXTRA_SIZE_LIMIT` (default `512`) limits the `extra` field size in bytes.
|
|
||||||
|
|
||||||
## Docker / Compose
|
|
||||||
|
|
||||||
- The `redis` service in docker-compose is renamed to `cache`
|
|
||||||
- Healthcheck URL updated: `http://127.0.0.1:8000/api/live/` → `http://127.0.0.1:8000/healthz`
|
|
||||||
- The default image stays `valkey/valkey:7-alpine` but operators can swap for any redis-compatible image
|
|
||||||
|
|
||||||
## CLI (`cryptgeon` npm package)
|
|
||||||
|
|
||||||
- Dropped `occulto` dependency — now uses `@noble/ciphers` + `@noble/hashes` internally
|
|
||||||
- Encryption changed from AES to **XChaCha20-Poly1305**
|
|
||||||
- The local `shared/` module removed — now imports from `@cryptgeon/shared` (workspace-internal)
|
|
||||||
- Notes created with v2 (AES) are **not readable** by v3 and vice versa
|
|
||||||
|
|
||||||
## Frontend
|
|
||||||
|
|
||||||
- Dropped `occulto` dependency
|
|
||||||
- Package import changed from `cryptgeon/shared` to `@cryptgeon/shared`
|
|
||||||
- Notes created in v2 are not accessible from the v3 frontend
|
|
||||||
|
|
||||||
## Storage
|
|
||||||
|
|
||||||
- Cache storage format changed from JSON blobs to hashes with atomic `HINCRBY` for view counting
|
|
||||||
- The per-note lock (`lock.rs`) is removed — no longer needed
|
|
||||||
- Existing v2 notes in cache are **not migrated** and will be inaccessible after upgrade
|
|
||||||
- Ensure cache is empty (or flush) before deploying v3
|
|
||||||
-323
@@ -1,323 +0,0 @@
|
|||||||
# v3 Plan
|
|
||||||
|
|
||||||
> Status: **Draft** — agreed on architecture, schema open for iteration.
|
|
||||||
>
|
|
||||||
> See also: [v3 Breaking Changes](./v3-breaking-changes.md) for the upgrade guide.
|
|
||||||
|
|
||||||
## Goals
|
|
||||||
|
|
||||||
- **XChaCha20-Poly1305** for encryption (replaces AES/`occulto`)
|
|
||||||
- **MessagePack** for all API request/response bodies (replaces JSON)
|
|
||||||
- **LZ4 compression** for note payloads (client-side, before encryption — pure JS, no wasm)
|
|
||||||
- **Cache hashes** (valkey or redis, both speak RESP) for storage — replaces JSON-blob-per-key
|
|
||||||
- **Clean break** from v1 — no backward compatibility, no v1 routes
|
|
||||||
- Remove all Redis references (env vars, service names, docs) in favor of the generic "cache" naming, so operators can choose valkey or redis
|
|
||||||
- Shared TypeScript package as the single source of truth for crypto + API client + types
|
|
||||||
|
|
||||||
## Non-goals
|
|
||||||
|
|
||||||
- Keeping v1 alive alongside v3
|
|
||||||
- Changing the backend language/framework (stays Rust + axum)
|
|
||||||
- Changing storage backend (stays valkey or redis via the `redis` crate — no separate crate)
|
|
||||||
- Publishing `@cryptgeon/shared` as a standalone npm package (workspace-internal for now)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1. Shared package — `@cryptgeon/shared`
|
|
||||||
|
|
||||||
Location: `packages/shared` (currently empty).
|
|
||||||
|
|
||||||
ESM-only, TypeScript-only. Consumed by both `packages/cli` and `packages/frontend` via workspace dependency.
|
|
||||||
|
|
||||||
### Dependencies
|
|
||||||
|
|
||||||
- `@noble/ciphers` — XChaCha20-Poly1305
|
|
||||||
- `@noble/hashes` — scrypt
|
|
||||||
- `@msgpack/msgpack` — encode/decode
|
|
||||||
- `lz4js` — LZ4 compression (pure JS, no wasm)
|
|
||||||
- `ky` — HTTP client
|
|
||||||
|
|
||||||
### Structure
|
|
||||||
|
|
||||||
```
|
|
||||||
packages/shared/src/
|
|
||||||
index.ts # re-exports
|
|
||||||
crypto.ts # key derivation, encrypt, decrypt
|
|
||||||
compression.ts # LZ4 compress / decompress
|
|
||||||
types.ts # Note, NoteMetadata, FileDTO, Status, etc.
|
|
||||||
api.ts # high-level client: create, info, view, status
|
|
||||||
api.test.ts # tests
|
|
||||||
crypto.test.ts # tests
|
|
||||||
compression.test.ts # tests
|
|
||||||
```
|
|
||||||
|
|
||||||
### `crypto.ts`
|
|
||||||
|
|
||||||
- `deriveKey(password: string): Uint8Array` — scrypt, N=2^15, r=8, p=1, dkLen=32, fixed app-specific salt
|
|
||||||
- `generateKey(): Uint8Array` — `randomBytes(32)`
|
|
||||||
- `encrypt(data: Uint8Array, key: Uint8Array): Uint8Array` — `managedNonce(xchacha20poly1305)(key).encrypt(data)`
|
|
||||||
- `decrypt(ciphertext: Uint8Array, key: Uint8Array): Uint8Array` — `managedNonce(xchacha20poly1305)(key).decrypt(ciphertext)`
|
|
||||||
|
|
||||||
> **Note (carried over from msgpack branch):** the v2 stub had a bug — `decrypt` passed `key` as a second arg to `chacha.decrypt`, which only takes ciphertext. v3 must not repeat this.
|
|
||||||
|
|
||||||
### `compression.ts`
|
|
||||||
|
|
||||||
- `compress(data: Uint8Array): Uint8Array` — LZ4 block format
|
|
||||||
- `decompress(data: Uint8Array): Uint8Array` — LZ4 block format
|
|
||||||
|
|
||||||
Compression is a **client-only** concern. The server never sees or knows about compression — it stores the encrypted `data` blob as opaque bytes. The pipeline is:
|
|
||||||
|
|
||||||
```
|
|
||||||
msgpack encode → LZ4 compress → XChaCha20-Poly1305 encrypt
|
|
||||||
XChaCha20-Poly1305 decrypt → LZ4 decompress → msgpack decode
|
|
||||||
```
|
|
||||||
|
|
||||||
Compression runs on the plaintext (inner msgpack), never on ciphertext — encrypted data is high-entropy and incompressible. Always-on for v3 (all clients share the same package, no interop flag needed).
|
|
||||||
|
|
||||||
### `api.ts`
|
|
||||||
|
|
||||||
High-level client. All requests/responses are msgpack (`Content-Type: application/msgpack`). Methods:
|
|
||||||
|
|
||||||
- `setOptions({ server })` / `getOptions()`
|
|
||||||
- `create(note, key): Promise<{ id: string }>` — encodes msgpack, compresses (LZ4), encrypts, POST `/api/v3/notes/`
|
|
||||||
- `info(id): Promise<NoteInfo>` — GET `/api/v3/notes/{id}`, returns metadata only (no `data`)
|
|
||||||
- `view(id, key): Promise<NotePublic>` — DELETE `/api/v3/notes/{id}`, decrypts `data`, decompresses (LZ4), decodes msgpack
|
|
||||||
- `status(): Promise<Status>` — GET `/api/v3/status` (still JSON — server config, not note data)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. Backend (Rust)
|
|
||||||
|
|
||||||
### 2.1 Storage — `store.rs` rewrite
|
|
||||||
|
|
||||||
Switch from JSON-blob-per-key to **cache hashes** (valkey or redis, both speak RESP):
|
|
||||||
|
|
||||||
```
|
|
||||||
Key: {CACHE_PREFIX}{id}
|
|
||||||
Fields:
|
|
||||||
views (i32) # remaining views, or absent
|
|
||||||
expiration (u32) # unix timestamp, or absent
|
|
||||||
type ("text"|"file")
|
|
||||||
derivation (msgpack bytes, optional) # scrypt salt+params if password-based
|
|
||||||
data (bytes) # encrypted msgpack blob
|
|
||||||
```
|
|
||||||
|
|
||||||
Functions:
|
|
||||||
|
|
||||||
- `set(id, note)` → `HSET` all fields + `EXPIRE` (if time-limited)
|
|
||||||
- `get_meta(id)` → `HMGET views expiration type derivation` — never touches `data` (cheap preview)
|
|
||||||
- `get_data(id)` → `HGET data` — only when consuming
|
|
||||||
- `decrement_view(id)` → `HINCRBY views -1` — **atomic**, see 2.2
|
|
||||||
- `del(id)` → `DEL`
|
|
||||||
- `can_reach_cache()` — health check (renamed from `can_reach_redis`)
|
|
||||||
|
|
||||||
Use `rmp-serde` for msgpack (de)serialization of note structs.
|
|
||||||
|
|
||||||
### 2.2 Remove `lock.rs`
|
|
||||||
|
|
||||||
The per-id `Mutex` map in `SharedState` existed only because the consume endpoint did non-atomic read-modify-write on `views`. With `HINCRBY` this is atomic at the cache level.
|
|
||||||
|
|
||||||
- Delete `packages/backend/src/lock.rs`
|
|
||||||
- Remove `SharedState` from `main.rs` (the `.with_state(shared_state)` call)
|
|
||||||
- Remove the lock map + `Arc`/`Mutex` imports
|
|
||||||
|
|
||||||
### 2.3 Rewrite `note/`
|
|
||||||
|
|
||||||
- `model.rs` — msgpack-compatible structs (derive `Serialize`/`Deserialize` for `rmp-serde`)
|
|
||||||
- `routes.rs` — three handlers:
|
|
||||||
|
|
||||||
#### `create` — `POST /api/v3/notes/`
|
|
||||||
|
|
||||||
- Accepts `application/msgpack` body (raw `Bytes`)
|
|
||||||
- Deserialize with rmp-serde
|
|
||||||
- Validate:
|
|
||||||
- At least one of `views`/`expiration` must be set
|
|
||||||
- `views` ≤ `MAX_VIEWS` and ≥ 1
|
|
||||||
- `expiration` ≤ `MAX_EXPIRATION` (minutes) and ≥ 1
|
|
||||||
- If `ALLOW_ADVANCED=false`: force `views=1, expiration=None`
|
|
||||||
- Store via `store::set`
|
|
||||||
- Return `{ id }` as msgpack
|
|
||||||
|
|
||||||
#### `preview` (info) — `GET /api/v3/notes/{id}`
|
|
||||||
|
|
||||||
- `store::get_meta(id)` — does not load `data`
|
|
||||||
- Return metadata as msgpack (no `data` field)
|
|
||||||
- `404` if not found
|
|
||||||
|
|
||||||
#### `view` (consume) — `DELETE /api/v3/notes/{id}`
|
|
||||||
|
|
||||||
- If `views` is set:
|
|
||||||
- `HINCRBY views -1` (atomic)
|
|
||||||
- If result ≤ 0: `HGET data`, `DEL` key, return data
|
|
||||||
- If result > 0: `HGET data`, return data (note survives for remaining views)
|
|
||||||
- If `views` is not set (time-only):
|
|
||||||
- `HGET data`, `DEL` key, return data
|
|
||||||
- Expiration handled lazily by cache (`EXPIRE` on the key) — no manual `if e < n` check on read
|
|
||||||
|
|
||||||
### 2.4 Config — `config.rs`
|
|
||||||
|
|
||||||
Rename:
|
|
||||||
- `REDIS` env → `CACHE`
|
|
||||||
- `REDIS_PREFIX` → `CACHE_PREFIX`
|
|
||||||
- `REDIS_CLIENT` static → `CACHE_CLIENT`
|
|
||||||
|
|
||||||
Everything else stays.
|
|
||||||
|
|
||||||
### 2.5 Health — `health/mod.rs`
|
|
||||||
|
|
||||||
- Rename `can_reach_redis` → `can_reach_cache`. Update panic message in `main.rs`.
|
|
||||||
- Move route from `/api/live` to `/healthz` (k8s standard). Not under `/api/v3/` — health checks are infrastructure, not API surface.
|
|
||||||
|
|
||||||
### 2.6 Status — `status/mod.rs`
|
|
||||||
|
|
||||||
Keep as JSON. It's server configuration, not note data — msgpack adds nothing. Frontend fetches once on load.
|
|
||||||
|
|
||||||
### 2.7 Dependencies — `Cargo.toml`
|
|
||||||
|
|
||||||
- Add `rmp-serde` (msgpack)
|
|
||||||
- Remove `serde_json` if no longer used (status endpoint still uses `Json<T>` which needs `serde_json` — keep)
|
|
||||||
- Keep `redis` crate (RESP client, works with valkey and redis)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. "Both" constraint (views AND expiration)
|
|
||||||
|
|
||||||
New in v3: a note can have **both** `views` and `expiration` set simultaneously.
|
|
||||||
|
|
||||||
Implementation:
|
|
||||||
- `views` decremented via `HINCRBY views -1` on each consume
|
|
||||||
- `expiration` set via key-level `EXPIRE` (unix timestamp → seconds remaining)
|
|
||||||
- Whichever trips first removes the note:
|
|
||||||
- Views hit 0 → we `DEL` on the last consume
|
|
||||||
- Time expires → cache lazily removes the key
|
|
||||||
- No read-time expiration check needed in application code
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. Infra / docs cleanup
|
|
||||||
|
|
||||||
- `docker-compose.dev.yaml`: rename `redis` service → `cache` (image stays `valkey/valkey:7-alpine`, operators can swap for redis)
|
|
||||||
- `docker-compose.yaml` (if present): same
|
|
||||||
- `Dockerfile`: `ENV REDIS=...` → `ENV CACHE=...`
|
|
||||||
- `package.json` (root): `dev:docker` script service name
|
|
||||||
- `README.md`, `README_ES.md`, `README_zh-CN.md`, `CONTRIBUTING.md`, `CHANGELOG.md`, `examples/*` — replace "redis" with "cache" (or "valkey/redis" where context calls for naming the implementation)
|
|
||||||
- `Cryptgeon.postman_collection.json` — update content types to `application/msgpack`
|
|
||||||
- `.env.dev` — update `REDIS` → `CACHE` if present
|
|
||||||
- Healthcheck URLs: update all `/api/live` references → `/healthz` (docker-compose files, README, postman collection)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. CLI (`packages/cli`)
|
|
||||||
|
|
||||||
- Drop `occulto` dependency
|
|
||||||
- Delete `packages/cli/src/shared/` (api.ts, adapters.ts, shared.ts) — replaced by `@cryptgeon/shared`
|
|
||||||
- `actions/upload.ts` and `actions/download.ts` call into `@cryptgeon/shared` API client
|
|
||||||
- Package still published as `cryptgeon` on npm
|
|
||||||
- `@cryptgeon/shared` stays workspace-internal (not published) for now
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 6. Frontend (`packages/frontend`)
|
|
||||||
|
|
||||||
- Drop `occulto` dependency
|
|
||||||
- Import from `@cryptgeon/shared` instead of `cryptgeon/shared`
|
|
||||||
- Update `package.json`: `"cryptgeon": "workspace:*"` → `"@cryptgeon/shared": "workspace:*"`
|
|
||||||
- Update files:
|
|
||||||
- `src/lib/views/Create.svelte`
|
|
||||||
- `src/lib/ui/ShowNote.svelte`
|
|
||||||
- `src/lib/ui/FileUpload.svelte`
|
|
||||||
- `src/lib/ui/PastedFilesPreview.svelte`
|
|
||||||
- `src/lib/ui/AdvancedParameters.svelte`
|
|
||||||
- `src/lib/stores/status.ts`
|
|
||||||
- `src/routes/note/[id]/+page.svelte`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 7. msgpack note schema — "matrioshka" design
|
|
||||||
|
|
||||||
The server is **agnostic to the content**. It only sees an outer envelope with metadata and an opaque encrypted blob. The content type (text vs. files) lives inside the encrypted inner layer, invisible to the server.
|
|
||||||
|
|
||||||
### Outer layer (server-visible)
|
|
||||||
|
|
||||||
```
|
|
||||||
{
|
|
||||||
meta: {
|
|
||||||
expiration: u32? # optional, unix timestamp
|
|
||||||
views: u32? # optional, remaining view count
|
|
||||||
extra: bytes? # optional, client-opaque, size-limited
|
|
||||||
}
|
|
||||||
data: bytes # encrypted inner msgpack blob
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
- `meta.expiration` / `meta.views`: at least one must be set; both can be set simultaneously (see section 3)
|
|
||||||
- `meta.extra`: opaque client-owned data the server stores and returns verbatim in preview, but never interprets. Used for `derivation` (scrypt salt + params) so the client knows at preview time whether to prompt for a password. Size-limited (e.g. 512 bytes) to prevent abuse.
|
|
||||||
|
|
||||||
### Inner layer (encrypted, client-only)
|
|
||||||
|
|
||||||
Inside the encrypted `data` blob, after decryption, is a msgpack union:
|
|
||||||
|
|
||||||
```
|
|
||||||
# Text note
|
|
||||||
{ type: "text", data: string }
|
|
||||||
|
|
||||||
# File note
|
|
||||||
{ type: "files", data: [{ name: string, mime: string, data: bytes }] }
|
|
||||||
```
|
|
||||||
|
|
||||||
The server never sees this structure — it stores/retrieves `data` as opaque bytes.
|
|
||||||
|
|
||||||
The inner msgpack blob is **LZ4-compressed before encryption** (see `compression.ts`). Full client pipeline: `msgpack encode → lz4 compress → xchacha20poly1305 encrypt`, reversed on consume. The server is unaware of compression — it only ever handles the encrypted `data` bytes.
|
|
||||||
|
|
||||||
### Endpoints
|
|
||||||
|
|
||||||
#### `POST /api/v3/notes/` — create
|
|
||||||
|
|
||||||
**Request** (msgpack): outer layer `{ meta: { expiration?, views?, extra? }, data }`
|
|
||||||
|
|
||||||
**Response** (msgpack): `{ id: string }`
|
|
||||||
|
|
||||||
#### `GET /api/v3/notes/{id}` — preview / info
|
|
||||||
|
|
||||||
**Response** (msgpack): `{ meta: { expiration?, views?, extra? } }`
|
|
||||||
|
|
||||||
Returns metadata only — does not load `data` from cache. Client inspects `meta.extra` to determine key derivation strategy (password vs. URL-fragment key) before consuming.
|
|
||||||
|
|
||||||
#### `DELETE /api/v3/notes/{id}` — view / consume
|
|
||||||
|
|
||||||
**Response** (msgpack): `{ meta: { expiration?, views?, extra? }, data: bytes }`
|
|
||||||
|
|
||||||
Returns the full envelope. Client decrypts `data` using key derived from `meta.extra` (if present) or URL fragment, then decodes the inner msgpack to get text/files.
|
|
||||||
|
|
||||||
#### `GET /api/v3/status` — server config
|
|
||||||
|
|
||||||
**Response** (JSON, not msgpack): server configuration, not note data. Kept as JSON for simplicity.
|
|
||||||
|
|
||||||
### Valkey hash field layout
|
|
||||||
|
|
||||||
```
|
|
||||||
Key: {CACHE_PREFIX}{id}
|
|
||||||
Fields:
|
|
||||||
views (i32) # remaining views, or absent
|
|
||||||
expiration (u32) # unix timestamp, or absent
|
|
||||||
extra (bytes) # client-opaque, size-limited
|
|
||||||
data (bytes) # encrypted msgpack blob
|
|
||||||
```
|
|
||||||
|
|
||||||
`get_meta(id)` does `HMGET views expiration extra` — never touches `data`.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 8. Implementation order
|
|
||||||
|
|
||||||
1. Shared package scaffolding (package.json, tsconfig, vitest config)
|
|
||||||
2. `crypto.ts` + tests
|
|
||||||
3. `compression.ts` + tests
|
|
||||||
4. `types.ts`
|
|
||||||
5. Backend: config rename + store rewrite + remove lock.rs
|
|
||||||
6. Backend: note routes rewrite (msgpack)
|
|
||||||
7. `api.ts` in shared (client) + tests
|
|
||||||
8. CLI rewrite (drop shared/, use @cryptgeon/shared)
|
|
||||||
9. Frontend migration
|
|
||||||
10. Infra/docs cleanup (cache rename, compose, Dockerfile)
|
|
||||||
11. Integration tests (playwright)
|
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
version: '3.8'
|
|
||||||
|
|
||||||
services:
|
services:
|
||||||
redis:
|
cache:
|
||||||
image: valkey/valkey:7-alpine
|
image: valkey/valkey:7-alpine
|
||||||
# This is required to stay in RAM only.
|
# This is required to stay in RAM only.
|
||||||
command: valkey-server --save "" --appendonly no
|
command: valkey-server --save "" --appendonly no
|
||||||
@@ -15,7 +14,7 @@ services:
|
|||||||
app:
|
app:
|
||||||
image: cupcakearmy/cryptgeon:latest
|
image: cupcakearmy/cryptgeon:latest
|
||||||
depends_on:
|
depends_on:
|
||||||
- redis
|
- cache
|
||||||
|
|
||||||
proxy:
|
proxy:
|
||||||
image: nginx:alpine
|
image: nginx:alpine
|
||||||
|
|||||||
+14
-19
@@ -25,27 +25,26 @@ This is a tiny guide to install cryptgeon on (probably) any unix system (and may
|
|||||||
```yaml
|
```yaml
|
||||||
# docker-compose.yaml
|
# docker-compose.yaml
|
||||||
|
|
||||||
version: '3.8'
|
|
||||||
services:
|
services:
|
||||||
traefik:
|
traefik:
|
||||||
image: traefik:2.6
|
image: traefik:2.6
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
ports:
|
ports:
|
||||||
- '80:80'
|
- "80:80"
|
||||||
- '443:443'
|
- "443:443"
|
||||||
volumes:
|
volumes:
|
||||||
- /var/run/docker.sock:/var/run/docker.sock
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
- ./traefik.yaml:/etc/traefik/traefik.yaml:ro
|
- ./traefik.yaml:/etc/traefik/traefik.yaml:ro
|
||||||
- ./data:/data
|
- ./data:/data
|
||||||
labels:
|
labels:
|
||||||
- 'traefik.enable=true'
|
- "traefik.enable=true"
|
||||||
|
|
||||||
# HTTP to HTTPS redirection
|
# HTTP to HTTPS redirection
|
||||||
- 'traefik.http.routers.http_catchall.rule=HostRegexp(`{any:.+}`)'
|
- "traefik.http.routers.http_catchall.rule=HostRegexp(`{any:.+}`)"
|
||||||
- 'traefik.http.routers.http_catchall.entrypoints=insecure'
|
- "traefik.http.routers.http_catchall.entrypoints=insecure"
|
||||||
- 'traefik.http.routers.http_catchall.middlewares=https_redirect'
|
- "traefik.http.routers.http_catchall.middlewares=https_redirect"
|
||||||
- 'traefik.http.middlewares.https_redirect.redirectscheme.scheme=https'
|
- "traefik.http.middlewares.https_redirect.redirectscheme.scheme=https"
|
||||||
- 'traefik.http.middlewares.https_redirect.redirectscheme.permanent=true'
|
- "traefik.http.middlewares.https_redirect.redirectscheme.permanent=true"
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
default:
|
default:
|
||||||
@@ -62,15 +61,15 @@ api:
|
|||||||
# Define HTTP and HTTPS entrypoint
|
# Define HTTP and HTTPS entrypoint
|
||||||
entryPoints:
|
entryPoints:
|
||||||
insecure:
|
insecure:
|
||||||
address: ':80'
|
address: ":80"
|
||||||
secure:
|
secure:
|
||||||
address: ':443'
|
address: ":443"
|
||||||
|
|
||||||
# Dynamic configuration will come from docker labels
|
# Dynamic configuration will come from docker labels
|
||||||
providers:
|
providers:
|
||||||
docker:
|
docker:
|
||||||
endpoint: 'unix:///var/run/docker.sock'
|
endpoint: "unix:///var/run/docker.sock"
|
||||||
network: 'proxy'
|
network: "proxy"
|
||||||
exposedByDefault: false
|
exposedByDefault: false
|
||||||
|
|
||||||
# Enable acme with http file challenge
|
# Enable acme with http file challenge
|
||||||
@@ -100,14 +99,12 @@ Create another docker-compose.yaml file in another folder. We will assume that t
|
|||||||
```
|
```
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
version: '3.8'
|
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
proxy:
|
proxy:
|
||||||
external: true
|
external: true
|
||||||
|
|
||||||
services:
|
services:
|
||||||
redis:
|
cache:
|
||||||
image: valkey/valkey:7-alpine
|
image: valkey/valkey:7-alpine
|
||||||
# This is required to stay in RAM only.
|
# This is required to stay in RAM only.
|
||||||
command: valkey-server --save "" --appendonly no
|
command: valkey-server --save "" --appendonly no
|
||||||
@@ -122,7 +119,7 @@ services:
|
|||||||
image: cupcakearmy/cryptgeon:latest
|
image: cupcakearmy/cryptgeon:latest
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
depends_on:
|
depends_on:
|
||||||
- redis
|
- cache
|
||||||
environment:
|
environment:
|
||||||
SIZE_LIMIT: 4 MiB
|
SIZE_LIMIT: 4 MiB
|
||||||
networks:
|
networks:
|
||||||
@@ -155,8 +152,6 @@ docker-compose up -d
|
|||||||
```yaml
|
```yaml
|
||||||
# docker-compose.yaml
|
# docker-compose.yaml
|
||||||
|
|
||||||
version: '3.8'
|
|
||||||
|
|
||||||
services:
|
services:
|
||||||
watchtower:
|
watchtower:
|
||||||
image: containrrr/watchtower
|
image: containrrr/watchtower
|
||||||
|
|||||||
@@ -9,14 +9,12 @@ Assumptions:
|
|||||||
- Domain name `example.org`.
|
- Domain name `example.org`.
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
version: '3.8'
|
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
proxy:
|
proxy:
|
||||||
external: true
|
external: true
|
||||||
|
|
||||||
services:
|
services:
|
||||||
redis:
|
cache:
|
||||||
image: valkey/valkey:7-alpine
|
image: valkey/valkey:7-alpine
|
||||||
# This is required to stay in RAM only.
|
# This is required to stay in RAM only.
|
||||||
command: valkey-server --save "" --appendonly no
|
command: valkey-server --save "" --appendonly no
|
||||||
@@ -31,7 +29,7 @@ services:
|
|||||||
image: cupcakearmy/cryptgeon:latest
|
image: cupcakearmy/cryptgeon:latest
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
depends_on:
|
depends_on:
|
||||||
- redis
|
- cache
|
||||||
networks:
|
networks:
|
||||||
- default
|
- default
|
||||||
- proxy
|
- proxy
|
||||||
@@ -60,7 +58,7 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- "/var/run/docker.sock:/var/run/docker.sock:ro"
|
- "/var/run/docker.sock:/var/run/docker.sock:ro"
|
||||||
|
|
||||||
redis:
|
cache:
|
||||||
image: valkey/valkey:7-alpine
|
image: valkey/valkey:7-alpine
|
||||||
# This is required to stay in RAM only.
|
# This is required to stay in RAM only.
|
||||||
command: valkey-server --save "" --appendonly no
|
command: valkey-server --save "" --appendonly no
|
||||||
@@ -74,7 +72,7 @@ services:
|
|||||||
cryptgeon:
|
cryptgeon:
|
||||||
image: cupcakearmy/cryptgeon
|
image: cupcakearmy/cryptgeon
|
||||||
depends_on:
|
depends_on:
|
||||||
- redis
|
- cache
|
||||||
labels:
|
labels:
|
||||||
- "traefik.enable=true"
|
- "traefik.enable=true"
|
||||||
- "traefik.http.routers.cryptgeon.rule=Host(`cryptgeon.localhost`)"
|
- "traefik.http.routers.cryptgeon.rule=Host(`cryptgeon.localhost`)"
|
||||||
|
|||||||
Generated
+1
-1
@@ -252,7 +252,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cryptgeon"
|
name = "cryptgeon"
|
||||||
version = "3.0.0"
|
version = "3.0.0-rc.2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"axum",
|
"axum",
|
||||||
"bs62",
|
"bs62",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "cryptgeon"
|
name = "cryptgeon"
|
||||||
version = "3.0.0"
|
version = "3.0.0-rc.2"
|
||||||
authors = ["cupcakearmy <hi@nicco.io>"]
|
authors = ["cupcakearmy <hi@nicco.io>"]
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
rust-version = "1.95"
|
rust-version = "1.95"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "cryptgeon",
|
"name": "cryptgeon",
|
||||||
"version": "3.0.0",
|
"version": "3.0.0-rc.2",
|
||||||
"homepage": "https://github.com/cupcakearmy/cryptgeon",
|
"homepage": "https://github.com/cupcakearmy/cryptgeon",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
@@ -21,21 +21,19 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "vp pack",
|
"build": "vp pack",
|
||||||
"dev": "vp pack --watch",
|
"dev": "vp pack --watch",
|
||||||
"prepublishOnly": "run-s build"
|
"prepublishOnly": "pnpm run build"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"devDependencies": {
|
||||||
"@cryptgeon/shared": "workspace:*",
|
"@cryptgeon/shared": "workspace:*",
|
||||||
"@msgpack/msgpack": "^3.1.3",
|
"@msgpack/msgpack": "^3.1.3",
|
||||||
"@commander-js/extra-typings": "^12.1.0",
|
"@commander-js/extra-typings": "^12.1.0",
|
||||||
"inquirer": "^9.3.8",
|
|
||||||
"mime": "^4.1.0",
|
|
||||||
"pretty-bytes": "^6.1.1"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@tsconfig/strictest": "catalog:",
|
"@tsconfig/strictest": "catalog:",
|
||||||
"@types/inquirer": "^9.0.9",
|
"@types/inquirer": "^9.0.9",
|
||||||
"@types/node": "^22.15.3",
|
"@types/node": "^22.15.3",
|
||||||
"commander": "^12.1.0",
|
"commander": "^12.1.0",
|
||||||
|
"inquirer": "^9.3.8",
|
||||||
|
"mime": "^4.1.0",
|
||||||
|
"pretty-bytes": "^6.1.1",
|
||||||
"typescript": "catalog:",
|
"typescript": "catalog:",
|
||||||
"vite-plus": "catalog:"
|
"vite-plus": "catalog:"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { access, constants, writeFile } from 'node:fs/promises'
|
|||||||
import { basename, resolve } from 'node:path'
|
import { basename, resolve } from 'node:path'
|
||||||
import { decode } from '@msgpack/msgpack'
|
import { decode } from '@msgpack/msgpack'
|
||||||
import pretty from 'pretty-bytes'
|
import pretty from 'pretty-bytes'
|
||||||
import { decrypt, deriveKey, setServer, info, get, decompress } from '@cryptgeon/shared'
|
import { deriveKey, setServer, info, get, unpackContent } from '@cryptgeon/shared'
|
||||||
|
|
||||||
export async function download(url: URL, all: boolean, suggestedPassword?: string) {
|
export async function download(url: URL, all: boolean, suggestedPassword?: string) {
|
||||||
setServer(url.origin)
|
setServer(url.origin)
|
||||||
@@ -32,8 +32,7 @@ export async function download(url: URL, all: boolean, suggestedPassword?: strin
|
|||||||
const note = await get(id)
|
const note = await get(id)
|
||||||
if (!note) throw new Error('Could not load note')
|
if (!note) throw new Error('Could not load note')
|
||||||
|
|
||||||
const decrypted = decrypt(note.data, key)
|
const content = unpackContent(note.data, key)
|
||||||
const content = decode(decompress(decrypted)) as any
|
|
||||||
|
|
||||||
switch (content.type) {
|
switch (content.type) {
|
||||||
case 'files':
|
case 'files':
|
||||||
|
|||||||
@@ -1,43 +1,38 @@
|
|||||||
import { readFile } from 'node:fs/promises'
|
import { readFile } from 'node:fs/promises'
|
||||||
import { basename } from 'node:path'
|
import { basename } from 'node:path'
|
||||||
|
|
||||||
import { encode } from '@msgpack/msgpack'
|
|
||||||
import mime from 'mime'
|
import mime from 'mime'
|
||||||
import { encrypt, generateKey, deriveKey, randomBytes, getServer, create, compress } from '@cryptgeon/shared'
|
import { getServer, create, packContent, type FileDTO } from '@cryptgeon/shared'
|
||||||
|
|
||||||
export type UploadOptions = { views?: number; expiration?: number; password?: string }
|
export type UploadOptions = { views?: number; expiration?: number; password?: string }
|
||||||
|
|
||||||
export async function upload(input: string | string[], options: UploadOptions): Promise<string> {
|
export async function upload(input: string | string[], options: UploadOptions): Promise<string> {
|
||||||
const { password, ...noteOptions } = options
|
const { password, ...noteOptions } = options
|
||||||
|
|
||||||
let key: Uint8Array
|
const payload = packContent(
|
||||||
let extra = new Uint8Array()
|
typeof input === 'string'
|
||||||
if (password) {
|
? { type: 'text', text: input }
|
||||||
const salt = randomBytes(16)
|
: { type: 'files', files: await fileDTOSfromPaths(input) },
|
||||||
key = deriveKey(password, salt)
|
password
|
||||||
extra = encode({ salt, N: 32768, r: 8, p: 1 })
|
)
|
||||||
} else {
|
|
||||||
key = generateKey()
|
|
||||||
}
|
|
||||||
|
|
||||||
let inner: Uint8Array
|
const result = await create({ meta: { ...noteOptions, extra: payload.extra }, data: payload.data })
|
||||||
if (typeof input === 'string') {
|
|
||||||
inner = encode({ type: 'text', data: input })
|
|
||||||
} else {
|
|
||||||
const files = await Promise.all(
|
|
||||||
input.map(async (path) => {
|
|
||||||
const data = new Uint8Array(await readFile(path))
|
|
||||||
const extension = path.substring(path.indexOf('.') + 1)
|
|
||||||
const type = mime.getType(extension) ?? 'application/octet-stream'
|
|
||||||
return { name: basename(path), mime: type, size: data.length, data }
|
|
||||||
})
|
|
||||||
)
|
|
||||||
inner = encode({ type: 'files', data: files })
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = encrypt(compress(inner), key)
|
|
||||||
const result = await create({ meta: { ...noteOptions, extra }, data })
|
|
||||||
let url = `${getServer()}/note/${result.id}`
|
let url = `${getServer()}/note/${result.id}`
|
||||||
if (!password) url += `#${Buffer.from(key).toString('hex')}`
|
if (!password) url += `#${Buffer.from(payload.key).toString('hex')}`
|
||||||
return url
|
return url
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fileDTOSfromPaths(paths: string[]): Promise<FileDTO[]> {
|
||||||
|
return Promise.all(
|
||||||
|
paths.map(async (path) => {
|
||||||
|
const extension = path.substring(path.indexOf('.') + 1)
|
||||||
|
const data = new Uint8Array(await readFile(path))
|
||||||
|
return {
|
||||||
|
name: basename(path),
|
||||||
|
mime: mime.getType(extension) ?? 'application/octet-stream',
|
||||||
|
size: data.length,
|
||||||
|
data,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
}
|
}
|
||||||
@@ -4,14 +4,12 @@
|
|||||||
|
|
||||||
import { status } from '$lib/stores/status'
|
import { status } from '$lib/stores/status'
|
||||||
|
|
||||||
// Due to encoding overhead (~35%) with base64
|
// Payload is raw bytes (msgpack + cipher), no base64 padding overhead.
|
||||||
// https://en.wikipedia.org/wiki/Base64
|
|
||||||
const overhead = 1 / 1.35
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<span>
|
<span>
|
||||||
{#if $status !== null}
|
{#if $status !== null}
|
||||||
{prettyBytes($status.max_size * overhead, { binary: true })}
|
{prettyBytes($status.max_size, { binary: true })}
|
||||||
{:else}
|
{:else}
|
||||||
{$_('common.loading')}
|
{$_('common.loading')}
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {
|
import {
|
||||||
deriveKey, generateKey, encrypt, randomBytes,
|
bytesToHex,
|
||||||
bytesToHex, encode, compress,
|
|
||||||
create as apiCreate,
|
create as apiCreate,
|
||||||
type FileDTO, type ServerNote
|
packContent,
|
||||||
|
type ServerNote
|
||||||
} from '@cryptgeon/shared'
|
} from '@cryptgeon/shared'
|
||||||
import { t } from 'svelte-intl-precompile'
|
import { t } from 'svelte-intl-precompile'
|
||||||
import { blur } from 'svelte/transition'
|
import { blur } from 'svelte/transition'
|
||||||
@@ -117,42 +117,29 @@
|
|||||||
try {
|
try {
|
||||||
loading = $t('common.encrypting')
|
loading = $t('common.encrypting')
|
||||||
|
|
||||||
const salt = customPassword ? randomBytes(16) : null
|
|
||||||
const key = customPassword
|
|
||||||
? deriveKey(customPassword, salt!)
|
|
||||||
: generateKey()
|
|
||||||
|
|
||||||
let inner: Uint8Array
|
|
||||||
if (isFile) {
|
if (isFile) {
|
||||||
if (files.length === 0) throw new EmptyContentError()
|
if (files.length === 0) throw new EmptyContentError()
|
||||||
inner = encode({ type: 'files', data: files })
|
} else if (textContent === '') {
|
||||||
} else {
|
throw new EmptyContentError()
|
||||||
if (textContent === '') throw new EmptyContentError()
|
|
||||||
inner = encode({ type: 'text', data: textContent })
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const originalSize =inner.byteLength
|
const payload = packContent(
|
||||||
const compressed = compress(inner)
|
isFile ? { type: 'files', files } : { type: 'text', text: textContent },
|
||||||
const compresseedSize= compressed.byteLength
|
customPassword || undefined
|
||||||
console.debug({originalSize, compresseedSize, ratio: originalSize/compresseedSize})
|
)
|
||||||
|
|
||||||
const data = encrypt(compress(inner), key)
|
|
||||||
const extra = customPassword
|
|
||||||
? encode({ salt: salt!, N: 32768, r: 8, p: 1 })
|
|
||||||
: new Uint8Array()
|
|
||||||
const serverNote: ServerNote = {
|
const serverNote: ServerNote = {
|
||||||
meta: {
|
meta: {
|
||||||
...(timeExpiration ? { expiration: parseInt(note.expiration as any) } : { views: parseInt(note.views as any) }),
|
...(timeExpiration ? { expiration: parseInt(note.expiration as any) } : { views: parseInt(note.views as any) }),
|
||||||
extra,
|
extra: payload.extra,
|
||||||
},
|
},
|
||||||
data,
|
data: payload.data,
|
||||||
}
|
}
|
||||||
|
|
||||||
loading = $t('common.uploading')
|
loading = $t('common.uploading')
|
||||||
const response = await apiCreate(serverNote)
|
const response = await apiCreate(serverNote)
|
||||||
result = {
|
result = {
|
||||||
id: response.id,
|
id: response.id,
|
||||||
password: customPassword ? undefined : bytesToHex(key),
|
password: customPassword ? undefined : bytesToHex(payload.key),
|
||||||
}
|
}
|
||||||
notify.success($t('home.messages.note_created'))
|
notify.success($t('home.messages.note_created'))
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { deriveKey, hexToBytes, decrypt, decode, decompress, info, get as apiGet, type FileDTO } from '@cryptgeon/shared'
|
import { deriveKey, hexToBytes, decode, info, get as apiGet, unpackContent, type FileDTO } from '@cryptgeon/shared'
|
||||||
import { onMount } from 'svelte'
|
import { onMount } from 'svelte'
|
||||||
import { t } from 'svelte-intl-precompile'
|
import { t } from 'svelte-intl-precompile'
|
||||||
|
|
||||||
@@ -69,8 +69,7 @@
|
|||||||
key = hexToBytes(password!)
|
key = hexToBytes(password!)
|
||||||
}
|
}
|
||||||
|
|
||||||
const decrypted = decrypt(serverNote.data, key)
|
const content = unpackContent(serverNote.data, key)
|
||||||
const content = decode(decompress(decrypted)) as any
|
|
||||||
|
|
||||||
switch (content.type) {
|
switch (content.type) {
|
||||||
case 'text':
|
case 'text':
|
||||||
|
|||||||
@@ -2,4 +2,5 @@ export * from "./crypto.js";
|
|||||||
export * from "./types.js";
|
export * from "./types.js";
|
||||||
export * from "./api.js";
|
export * from "./api.js";
|
||||||
export * from "./compression.js";
|
export * from "./compression.js";
|
||||||
|
export * from "./payload.js";
|
||||||
export { encode, decode } from "@msgpack/msgpack";
|
export { encode, decode } from "@msgpack/msgpack";
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { packContent, unpackContent } from "./payload";
|
||||||
|
import { bytesToUtf8, utf8ToBytes } from "./crypto";
|
||||||
|
|
||||||
|
describe("payload", () => {
|
||||||
|
it("round-trips a text note through the full pipeline", () => {
|
||||||
|
const { data, extra, key } = packContent({ type: "text", text: "hello world" });
|
||||||
|
expect(extra.length).toBe(0);
|
||||||
|
const content = unpackContent(data, key);
|
||||||
|
expect(content).toEqual({ type: "text", data: "hello world" });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("round-trips with a password and sets extra", () => {
|
||||||
|
const { data, extra, key } = packContent({ type: "text", text: "secret" }, "pw123");
|
||||||
|
expect(extra.length).toBeGreaterThan(0);
|
||||||
|
const content = unpackContent(data, key);
|
||||||
|
expect(content).toEqual({ type: "text", data: "secret" });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("round-trips files (FileDTO)", () => {
|
||||||
|
const file = { name: "a.txt", mime: "text/plain", size: 5, data: utf8ToBytes("hello") };
|
||||||
|
const { data, key } = packContent({ type: "files", files: [file] });
|
||||||
|
const content = unpackContent(data, key);
|
||||||
|
expect(content.type).toBe("files");
|
||||||
|
if (content.type === "files") {
|
||||||
|
expect(content.data).toHaveLength(1);
|
||||||
|
expect(content.data[0]!.name).toBe("a.txt");
|
||||||
|
expect(bytesToUtf8(content.data[0]!.data)).toBe("hello");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
import { encode, decode } from "@msgpack/msgpack";
|
||||||
|
import { compress, decompress } from "./compression.js";
|
||||||
|
import {
|
||||||
|
deriveKey,
|
||||||
|
encrypt,
|
||||||
|
decrypt,
|
||||||
|
generateKey,
|
||||||
|
randomBytes,
|
||||||
|
} from "./crypto.js";
|
||||||
|
import type { FileDTO, NoteContent } from "./types.js";
|
||||||
|
|
||||||
|
export type NoteInput =
|
||||||
|
| { type: "text"; text: string }
|
||||||
|
| { type: "files"; files: FileDTO[] };
|
||||||
|
|
||||||
|
export type PackResult = {
|
||||||
|
data: Uint8Array;
|
||||||
|
extra: Uint8Array;
|
||||||
|
key: Uint8Array;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function packContent(input: NoteInput, password?: string): PackResult {
|
||||||
|
let key: Uint8Array;
|
||||||
|
let extra: Uint8Array;
|
||||||
|
if (password) {
|
||||||
|
const salt = randomBytes(16);
|
||||||
|
key = deriveKey(password, salt);
|
||||||
|
extra = encode({ salt, N: 32768, r: 8, p: 1 });
|
||||||
|
} else {
|
||||||
|
key = generateKey();
|
||||||
|
extra = new Uint8Array();
|
||||||
|
}
|
||||||
|
|
||||||
|
const content: NoteContent =
|
||||||
|
input.type === "text"
|
||||||
|
? { type: "text", data: input.text }
|
||||||
|
: { type: "files", data: input.files };
|
||||||
|
|
||||||
|
const encoded = encrypt(compress(encode(content)), key);
|
||||||
|
return { data: encoded, extra, key };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function unpackContent(data: Uint8Array, key: Uint8Array): NoteContent {
|
||||||
|
const content = decode(decompress(decrypt(data, key))) as NoteContent;
|
||||||
|
if (content.type !== "text" && content.type !== "files") {
|
||||||
|
throw new Error("Unknown content type");
|
||||||
|
}
|
||||||
|
return content;
|
||||||
|
}
|
||||||
Generated
+10
-11
@@ -30,7 +30,7 @@ importers:
|
|||||||
packages/backend: {}
|
packages/backend: {}
|
||||||
|
|
||||||
packages/cli:
|
packages/cli:
|
||||||
dependencies:
|
devDependencies:
|
||||||
'@commander-js/extra-typings':
|
'@commander-js/extra-typings':
|
||||||
specifier: ^12.1.0
|
specifier: ^12.1.0
|
||||||
version: 12.1.0(commander@12.1.0)
|
version: 12.1.0(commander@12.1.0)
|
||||||
@@ -40,16 +40,6 @@ importers:
|
|||||||
'@msgpack/msgpack':
|
'@msgpack/msgpack':
|
||||||
specifier: ^3.1.3
|
specifier: ^3.1.3
|
||||||
version: 3.1.3
|
version: 3.1.3
|
||||||
inquirer:
|
|
||||||
specifier: ^9.3.8
|
|
||||||
version: 9.3.8(@types/node@22.20.0)
|
|
||||||
mime:
|
|
||||||
specifier: ^4.1.0
|
|
||||||
version: 4.1.0
|
|
||||||
pretty-bytes:
|
|
||||||
specifier: ^6.1.1
|
|
||||||
version: 6.1.1
|
|
||||||
devDependencies:
|
|
||||||
'@tsconfig/strictest':
|
'@tsconfig/strictest':
|
||||||
specifier: 'catalog:'
|
specifier: 'catalog:'
|
||||||
version: 2.0.8
|
version: 2.0.8
|
||||||
@@ -62,6 +52,15 @@ importers:
|
|||||||
commander:
|
commander:
|
||||||
specifier: ^12.1.0
|
specifier: ^12.1.0
|
||||||
version: 12.1.0
|
version: 12.1.0
|
||||||
|
inquirer:
|
||||||
|
specifier: ^9.3.8
|
||||||
|
version: 9.3.8(@types/node@22.20.0)
|
||||||
|
mime:
|
||||||
|
specifier: ^4.1.0
|
||||||
|
version: 4.1.0
|
||||||
|
pretty-bytes:
|
||||||
|
specifier: ^6.1.1
|
||||||
|
version: 6.1.1
|
||||||
typescript:
|
typescript:
|
||||||
specifier: 'catalog:'
|
specifier: 'catalog:'
|
||||||
version: 7.0.2
|
version: 7.0.2
|
||||||
|
|||||||
Reference in New Issue
Block a user