Compare commits

...

367 Commits
v1.0.1 ... main

Author SHA1 Message Date
482795dd9a Merge pull request #181 from cupcakearmy/dependabot/cargo/packages/backend/cargo-362f336499
Bump ring from 0.16.20 to 0.17.12 in /packages/backend in the cargo group across 1 directory
2025-03-08 10:34:30 +01:00
dependabot[bot]
2907e7c002 Bump ring in /packages/backend in the cargo group across 1 directory
Bumps the cargo group with 1 update in the /packages/backend directory: [ring](https://github.com/briansmith/ring).


Updates `ring` from 0.16.20 to 0.17.12
- [Changelog](https://github.com/briansmith/ring/blob/main/RELEASES.md)
- [Commits](https://github.com/briansmith/ring/commits)

---
updated-dependencies:
- dependency-name: ring
  dependency-type: direct:production
  dependency-group: cargo
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-07 16:49:50 +00:00
4cc9d8a758 Merge pull request #179 from larsgerber/main
docs(compose): prevent anonymous volume creation
2025-03-02 21:50:52 +01:00
Lars Gerber
d652c4ee1e docs(compose): prevent anonymous volume creation 2025-03-01 21:28:34 +01:00
096be03966 Merge pull request #178 from cupcakearmy/176-ram-only
update docs about ram only redis
2025-02-27 20:13:09 +01:00
c53cde6886 Merge branch 'main' into 176-ram-only 2025-02-27 20:06:56 +01:00
0fa5a35dae update some versions 2025-02-27 20:00:38 +01:00
ebbb4efa04 version bump 2025-02-27 19:40:45 +01:00
a248440bfd Merge pull request #177 from cupcakearmy/password-eye-toggle
fix: password eye toggle not working
2025-02-27 19:38:37 +01:00
a1db60d159 update docs about ram only redis 2025-02-27 19:38:07 +01:00
c2653bee84 fix: password eye toggle not working 2025-02-27 19:28:16 +01:00
a2d2acc5de Merge pull request #168 from cupcakearmy/dependabot/npm_and_yarn/npm_and_yarn-545022be4d
Bump vite from 6.0.7 to 6.0.9 in the npm_and_yarn group across 1 directory
2025-01-22 17:58:41 +01:00
dependabot[bot]
4cc821150d Bump vite in the npm_and_yarn group across 1 directory
Bumps the npm_and_yarn group with 1 update in the / directory: [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite).


Updates `vite` from 6.0.7 to 6.0.9
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v6.0.9/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-type: direct:development
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-01-21 21:00:55 +00:00
e7fb844f66 Merge pull request #167 from cupcakearmy/svelte-5
some frontend love
2025-01-18 14:33:46 +01:00
567a0bed68 Update README.md 2025-01-18 03:52:13 +01:00
c13e53404c add csp draft 2025-01-17 18:48:28 +01:00
82862f0e3e version bump 2025-01-17 18:13:49 +01:00
e20f4626e7 Merge remote-tracking branch 'origin/main' into svelte-5 2025-01-17 18:11:34 +01:00
e440e4b7e0 update to svelte 5 2025-01-17 18:11:26 +01:00
808d846737 remove proxy 2025-01-17 18:11:03 +01:00
63c16a797b Merge pull request #166 from cupcakearmy/fix-race-condition
fix: introduce locks for delete endpoint to guarantee view counter
2025-01-17 18:01:30 +01:00
ea50590532 fix: introduce locks for delete endpoint to guarantee view counter 2025-01-17 17:34:32 +01:00
b22c3122d7 Merge pull request #163 from werewolfboy13/update-redis-link
Update Redis documentation link in Docker Compose file
2025-01-15 16:11:47 +01:00
Marek
18af2b2f45 Update Redis documentation link in Docker Compose file
Fixes #162

---

For more details, open the [Copilot Workspace session](https://copilot-workspace.githubnext.com/cupcakearmy/cryptgeon/issues/162?shareId=XXXX-XXXX-XXXX-XXXX).
2025-01-09 04:08:29 -06:00
c2b557246b Merge pull request #157 from Jerry-Shr/main
Add Chinese (zh-TW) translations
2025-01-02 09:29:22 +01:00
df9cd08473 version bump 2025-01-02 09:27:36 +01:00
JerryShr
0b8e1d1b2e Add Chinese (zh-TW) translations 2024-12-05 22:23:44 +08:00
70481341b9 Update README.md 2024-10-07 10:58:57 +02:00
6271ec1ee9 add basic auth example 2024-10-07 10:58:41 +02:00
c7ec587a2d bump version 2024-09-27 19:59:39 +00:00
3e8e82f51c Merge pull request #153 from scaedufax/imprint_html
Added Option to set a custom HTML Imprint
2024-09-27 21:57:44 +02:00
Uli
c314d4b485 Merge branch 'cupcakearmy:main' into imprint_html 2024-09-25 10:19:13 +02:00
Uli Roth
57ea5f0b28 added imprint_html option 2024-09-24 10:25:15 +02:00
Uli Roth
fca8761515 Added option to have an imprint
The environment Variable IMPRINT_URL simply adds a /imprint button in footer to the url
2024-09-24 10:15:22 +02:00
a47b8a482c Merge pull request #151 from cupcakearmy/150-type-button
150 type button
2024-09-20 20:24:26 +02:00
847fc9677d bump version 2024-09-20 18:16:01 +00:00
6979be0c4a bump pnpm version 2024-09-20 18:15:56 +00:00
f61d3ece8b add type button to element 2024-09-20 18:15:38 +00:00
14d3e9eb03 add raycast links 2024-09-03 15:57:29 +02:00
7c6ba654f6 correct version bump 2024-09-02 10:28:08 +02:00
cb20224317 version bumps 2024-09-02 10:27:26 +02:00
085b1c20df Merge pull request #148 from cupcakearmy/move-shared-package-into-cli
Move shared package into cli
2024-09-02 10:26:35 +02:00
4b1f939281 remove useless dependecies and use cli instead of package 2024-09-02 10:20:03 +02:00
7b919f2a53 move shared package to cli 2024-09-02 10:19:35 +02:00
868b49c1c3 add guide 2024-08-27 15:11:18 +02:00
6b5eea34a1 Merge pull request #147 from cupcakearmy/actix-2-axum
Actix 2 axum
2024-08-27 15:06:29 +02:00
d70bee14af version bump 2024-08-27 14:52:17 +02:00
4960260076 also disable icon in text 2024-08-27 14:49:08 +02:00
3247a0cfca skip expiration in safari 2024-08-27 14:35:39 +02:00
9527a499ed skip size 2024-08-27 13:26:04 +02:00
b5590bb5ef add retry 2024-08-27 11:45:43 +02:00
7691dc80f8 use pnpm command 2024-08-27 10:56:21 +02:00
f8d8fa05b0 new message 2024-08-27 10:55:59 +02:00
7aae690850 cleanup imports 2024-08-27 10:55:51 +02:00
e15d9eb537 upload artifacts 2024-08-27 00:42:21 +02:00
7fe70a6f3b revert 2024-08-27 00:38:56 +02:00
d86ef79abd use container 2024-08-27 00:36:48 +02:00
23446a4c74 Merge branch 'main' into actix-2-axum 2024-08-27 00:32:28 +02:00
ee92928d1f pipeline 2024-08-27 00:32:17 +02:00
b60a079bbe Merge pull request #140 from elfakyn/main
Fix typos in english localization
2024-08-27 00:23:28 +02:00
ba474dff31 more robust config 2024-08-27 00:22:41 +02:00
3cb002ee33 Merge branch 'main' into main 2024-08-27 00:10:44 +02:00
a10d8735dd cleanup tests 2024-08-27 00:09:51 +02:00
dfa2401eea aria check 2024-08-25 22:22:45 +02:00
ea58d89f98 cleanup 2024-08-25 22:22:13 +02:00
eaca1a981d cleanup 2024-08-25 22:21:45 +02:00
199755d18e timeout 2024-08-25 22:20:46 +02:00
724d0709d3 .env.dev 2024-08-25 22:20:29 +02:00
bd5acca97a use axum body limit 2024-08-25 22:19:41 +02:00
a0a99cd3cc cleanup status variable 2024-08-25 22:18:31 +02:00
c3794fa2b6 port 3000 2024-08-23 14:27:59 +02:00
f9962c76c1 enable to big 2024-08-23 14:27:52 +02:00
c2b81bc04d refactor to use axum 2024-08-23 14:27:47 +02:00
a45f6a3772 use 3000 port 2024-08-23 14:27:17 +02:00
2006be0434 Merge pull request #145 from cupcakearmy/better-programmatic-access
better programmatic access
2024-08-23 11:19:57 +02:00
ca72e94e3c update node and playwright 2024-08-23 11:02:30 +02:00
dbcb3870aa fix tests 2024-08-23 11:01:57 +02:00
3ea176cc1f add build cli 2024-08-23 09:59:06 +02:00
145f9ef18f right package stuff 2024-08-22 20:21:05 +02:00
784c54236b better programmatic access 2024-08-22 20:01:14 +02:00
5648c76f78 Merge pull request #144 from cupcakearmy/update-rust
update rust
2024-08-22 19:57:39 +02:00
7761c795df update rust 2024-08-22 18:42:44 +02:00
4aadeb492a maintenance 2024-08-22 18:40:56 +02:00
0d9f3fe9c7 Merge pull request #130 from DDd-Devops/add-redis-tls-feature
Add redis tls feature 'rediss://'
2024-08-22 18:30:58 +02:00
Arya ';--bscriptalert(":)")/script
8ccfdd6e2e fix typos in english localization 2024-07-04 18:00:06 -04:00
f790438104 add french blog post 2024-06-16 22:34:44 +02:00
5936f4588c Merge pull request #133 from DDd-Devops/improve-french-translations
improve french translations wording
2024-06-16 22:31:34 +02:00
Thomas Chrétien
d3c04f8fda improve french translations wording 2024-06-14 14:26:02 +00:00
Matthieu Guegan
f8c17487bd Support dynamically-linked and/or native musl targets
See https://github.com/rust-lang/rust/pull/40113#issuecomment-323193341
2024-05-16 09:55:04 +02:00
Matthieu Guegan
ed3e5f48a0 Fix wrong type due to updated lib
See https://github.com/redis-rs/redis-rs/pull/589
2024-05-16 09:47:01 +02:00
Matthieu Guegan
e08c9d1871 Bump redis crate to 0.25.2
This will enable TLS feature
2024-05-16 09:45:26 +02:00
6d2150b0b6 version bump 2024-05-04 16:06:40 +02:00
3a68693be1 Merge pull request #128 from cbin/main
change locales
2024-04-26 13:45:17 +02:00
Oleg Salnikov
a612eec220 Add files via upload 2024-04-24 18:31:01 +03:00
98d3b0d394 Merge pull request #127 from zocimek/main
fix key for home.new_note_notice
2024-04-22 09:38:49 +02:00
Łukasz Pospiech
6aed2e2756 fix key for home.new_note_notice 2024-04-18 12:39:00 +02:00
6bb527198a Merge pull request #123 from zocimek/patch-1
Create polish translation
2024-04-10 16:57:18 +02:00
Łukasz Pospiech
7050389316 Create polish translation 2024-04-05 14:00:58 +02:00
0725a0c6f7 Merge pull request #122 from cupcakearmy/dependabot/npm_and_yarn/npm_and_yarn-security-group-e93d6eacd9
Bump the npm_and_yarn group across 1 directory with 1 update
2024-04-05 09:37:44 +02:00
dependabot[bot]
c8efcc04fc Bump the npm_and_yarn group across 1 directory with 1 update
Bumps the npm_and_yarn group with 1 update in the / directory: [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite).


Updates `vite` from 5.1.4 to 5.1.7
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/v5.1.7/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v5.1.7/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-type: direct:development
  dependency-group: npm_and_yarn-security-group
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-04-03 17:38:45 +00:00
19bf155653 Merge pull request #120 from MairusuPawa/patch-1
Update fr.json
2024-03-24 20:57:00 +01:00
MairusuPawa
9a4e84db62 Update fr.json
Original translation confuses "crypter" and "chiffrer".
2024-03-23 16:19:29 +01:00
32cd3843a7 version bump 2024-03-21 15:55:21 +01:00
9b48d39c82 Merge pull request #119 from compasspathways/main
Misc improvements including `ALLOW_FILES` and `NEW_NOTE_NOTICE`
2024-03-19 16:04:04 +01:00
Tamás Kádár
239e950f8e NEW_NOTE_NOTICE -> THEME_NEW_NOTE_NOTICE 2024-03-19 12:06:46 +00:00
Tamás Kádár
b00846ce9d clicking the logo after the creation of the note should do a reset 2024-03-15 14:30:55 +00:00
Tamás Kádár
e70f06f99f NEW_NOTE_NOTICE feature flag 2024-03-15 14:26:33 +00:00
Tamás Kádár
4781882c72 fix extra space in /about 2024-03-15 14:19:30 +00:00
Tamás Kádár
549dfb55db update @types/node 2024-03-15 14:16:02 +00:00
Tamás Kádár
2653a4bccf ALLOW_FILES flag 2024-03-15 14:14:17 +00:00
Tamás Kádár
7213e6c690 Minor Dockerfile fixes 2024-03-15 14:11:34 +00:00
Tamás Kádár
8a5f667ff6 Fix file-saver CommonJS error 2024-03-15 13:53:12 +00:00
fc3938701e add reset translation 2024-03-04 18:46:00 +01:00
23b4f81dac Merge pull request #118 from codiflow/codiflow-patch-1
Update de.json
2024-03-04 18:42:39 +01:00
7c68620d8b Merge branch 'main' into codiflow-patch-1 2024-03-04 18:32:46 +01:00
eb76fe085a rm pkg and update node version 2024-03-04 18:32:03 +01:00
Christian
38540b33f2 Update de.json
Fixed some translations in the German language strings
2024-03-04 17:39:35 +01:00
39a9ac0dad version bump 2024-03-04 15:25:02 +01:00
ff1b5d500b Merge pull request #117 from cupcakearmy/also-expose-api-methods-for-programmatic-usage
Also expose api methods for programmatic usage
2024-03-04 15:24:02 +01:00
1698abe2eb pipeline 2024-03-04 15:15:47 +01:00
3036927a45 update dependencies 2024-03-03 01:50:52 +01:00
f9c26ba81c change entry point for cli test 2024-03-03 01:35:17 +01:00
752e68e213 revert to node 18 2024-03-03 01:34:08 +01:00
6eb3a59e33 use package.json pnpm version 2024-03-03 01:34:03 +01:00
1a2728d21f also expose internal shared functionality for external usage 2024-03-03 01:22:39 +01:00
a37a0932e0 translations 2024-02-26 17:19:29 +01:00
71a33a7939 Merge pull request #111 from cupcakearmy/dependabot/cargo/packages/backend/zerocopy-0.7.31
Bump zerocopy from 0.7.21 to 0.7.31 in /packages/backend
2024-02-09 14:47:47 +01:00
dependabot[bot]
83033a4b85 Bump zerocopy from 0.7.21 to 0.7.31 in /packages/backend
Bumps [zerocopy](https://github.com/google/zerocopy) from 0.7.21 to 0.7.31.
- [Release notes](https://github.com/google/zerocopy/releases)
- [Changelog](https://github.com/google/zerocopy/blob/main/CHANGELOG.md)
- [Commits](https://github.com/google/zerocopy/compare/v0.7.21...v0.7.31)

---
updated-dependencies:
- dependency-name: zerocopy
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-12-15 03:50:33 +00:00
40570bbbaf Merge pull request #108 from codiflow/patch-1
Updated German language strings
2023-12-13 21:22:44 +01:00
Christian
f591e589d0 Updated German language strings 2023-12-04 16:01:09 +01:00
d1eebb04f3 Merge pull request #107 from pgschk/locale-fixes
Fixes to locales en and de
2023-11-08 13:49:13 +01:00
pgschk
5a76ea7778 Update locale de
Fix some translation errors
2023-11-01 23:29:02 +01:00
pgschk
45a1af7569 Fix locale en
Fix spelling: to -> too, it's -> its
2023-11-01 23:18:58 +01:00
9578b2bed2 Merge pull request #106 from cupcakearmy/2.4.0
2.4.0
2023-11-01 15:57:12 +01:00
f94e4e3858 sparse bundler is enabled by default now 2023-11-01 15:26:55 +01:00
ffa2c49ea3 don't insert html anymore, use separate link section 2023-11-01 15:26:45 +01:00
0d850aadfc update deps 2023-11-01 15:26:33 +01:00
a9c646c981 update packages 2023-11-01 15:26:20 +01:00
a2803a13da update deps 2023-08-14 10:22:09 +02:00
deef56776d maintenance 2023-08-14 10:12:26 +02:00
b089323990 Merge pull request #97 from MVS/grammar-patch
Update en.json
2023-08-04 09:51:17 +02:00
MVS
6002ede685 Update en.json
Small tweaks to grammar
2023-08-03 12:07:50 -05:00
8a444ceb88 Merge pull request #95 from cbin/patch-1
Update ru.json
2023-07-19 16:59:50 +02:00
Oleg Salnikov
1e01ccb65a Update ru.json
minor changes
2023-07-18 20:59:24 +03:00
2dfa9dd248 Merge pull request #94 from Rooyca/main
Spanish translation of about page
2023-07-13 14:37:59 +02:00
Ronald
618e914b55 Add README_ES.md 2023-07-09 14:09:10 -05:00
Ronald
86f596fa4b Add README_ES.md 2023-07-09 14:07:40 -05:00
Ronald
dcb4613f66 Create README_ES.md 2023-07-09 14:05:59 -05:00
c46f80aaa0 Merge pull request #93 from cupcakearmy/feat/92-health-check
Feat/92 health check
2023-06-23 10:34:18 +02:00
e2711cc887 add healthcheck endpoint and startup check 2023-06-23 10:17:13 +02:00
e02224216a add changelog 2023-06-23 10:16:28 +02:00
1b0d5449a0 update postman collection 2023-06-23 10:16:13 +02:00
9695d3a63f version bumps 2023-06-23 10:16:03 +02:00
22d4efb03e add healthcheck examples 2023-06-23 10:15:31 +02:00
97741ed73f add curl for health check 2023-06-23 10:15:14 +02:00
c9e5de0f37 about page spacing 2023-06-02 23:51:54 +02:00
dc1c03d912 Merge pull request #90 from cupcakearmy/feature/52-Add-note-id-size-option
feat: add note id size option
2023-05-30 10:31:12 +02:00
2a75acae3f docs 2023-05-30 09:43:41 +02:00
815ac4e8ba changelog 2023-05-30 09:43:31 +02:00
7c85c1e621 version bump 2023-05-30 09:43:26 +02:00
a323d48c41 feat: add note id size option 2023-05-29 16:34:59 +02:00
2bff6a37db add some metadata 2023-05-26 01:10:22 +02:00
f8223dfc62 enable sparse bundle 2023-05-26 00:21:50 +02:00
063d073c27 fix pipeline 2023-05-25 23:54:59 +02:00
ac32b97383 Merge pull request #89 from cupcakearmy/69/password
69/password
2023-05-25 23:47:08 +02:00
9c9c23d958 version bump 2023-05-25 23:29:09 +02:00
92893a5b2d github actions 2023-05-25 23:29:05 +02:00
ac68f4a540 docs 2023-05-25 19:06:07 +02:00
83b2fa5372 version bump 2023-05-25 18:15:31 +02:00
3c86f3f3be update pnpm version 2023-05-25 18:15:18 +02:00
80e64ad207 fix types 2023-05-25 18:15:05 +02:00
a5809c216c fix scripts 2023-05-25 10:17:08 +02:00
fb95a68b0d test files in cli and cross with password 2023-05-25 10:17:01 +02:00
b43b802221 add --all option, stdin and password option 2023-05-25 10:16:44 +02:00
2e89007c83 add test ids 2023-05-25 10:16:12 +02:00
fdc2722fb9 add password to frontend 2023-05-23 09:39:19 +02:00
6da28a701e translations 2023-05-23 09:39:06 +02:00
e6d1e0f44a add password to CLI 2023-05-23 09:39:00 +02:00
6000553b95 include meta in info endpoint 2023-05-23 09:38:23 +02:00
85204776d7 demo postman collection 2023-05-23 09:38:00 +02:00
c2bfe9dd0d add derivation to metadata 2023-05-23 09:37:49 +02:00
b05841a562 add timeout 2023-05-23 09:37:33 +02:00
d7e5a34b14 CLI (#84)
* move to packages

* update deps

* update deps

* actions maintenance

* don't use blob

* cli

* fix default import

* use synthetic default imports

* remove comment

* cli packaging

* node 18 guard

* packages

* build system

* testing

* test pipeline

* pipelines

* changelog

* version bump

* update locales

* update deps

* update deps

* update dependecies
2023-05-14 13:52:47 +02:00
13dfd933af Add tutorial (#83) 2023-05-13 08:41:05 +02:00
Oleg Salnikov
74840416f1 locales (#85) 2023-05-13 08:40:50 +02:00
luolongfei
9aaad5b910 update zh.json file. (#79) 2023-03-02 09:55:50 +01:00
dependabot[bot]
c246207420 Bump tokio from 1.24.1 to 1.25.0 in /packages/backend (#75)
Bumps [tokio](https://github.com/tokio-rs/tokio) from 1.24.1 to 1.25.0.
- [Release notes](https://github.com/tokio-rs/tokio/releases)
- [Commits](https://github.com/tokio-rs/tokio/compare/tokio-1.24.1...tokio-1.25.0)

---
updated-dependencies:
- dependency-name: tokio
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-02-13 19:21:06 +01:00
7ee1b8370a update deps 2023-01-14 18:54:27 +01:00
e7750699cc chose: changelog 2023-01-14 18:40:45 +01:00
e14042ea28 chinese language code 2023-01-14 18:40:16 +01:00
6fb7518b6a moved to occulto 2023-01-13 21:24:27 +01:00
436ae2a7e5 move dev ports 2023-01-13 19:36:26 +01:00
fe5ce580ab locales 2023-01-04 19:45:25 +01:00
0f882da5d1 bump version 2023-01-04 19:45:18 +01:00
ad6f136dd0 qr code 2023-01-04 19:40:37 +01:00
da527a0857 2.0.7 2022-12-26 22:54:47 +01:00
a95931ae77 update deps 2022-12-26 18:00:00 +01:00
d6c2236673 update deps 2022-12-26 12:59:37 +01:00
42a8ab5d0f test command 2022-11-15 13:15:34 +01:00
0934808a59 testing 2022-11-15 13:05:13 +01:00
88ea828b66 upload 2022-11-15 12:35:02 +01:00
41ed5c0e23 ci 2022-11-15 12:17:52 +01:00
0a98481991 use npx 2022-11-15 12:13:03 +01:00
5d62c48a35 don't use pnpm 2022-11-14 16:26:51 +01:00
0ab39023b0 test 2022-11-14 16:16:50 +01:00
7b202962e8 testing 2022-11-14 15:55:49 +01:00
7a045b3f34 test on docker image 2022-11-14 15:47:12 +01:00
cb80c8bfe4 changelog 2022-11-12 14:40:21 +01:00
74c3197e47 update dependencies and fix some a11y issues 2022-11-12 14:40:17 +01:00
6ae927ce71 update version and dependencies 2022-11-12 13:55:33 +01:00
9d13e607f5 #66 set minimum 2022-11-12 13:42:09 +01:00
0db3ef4a1f changelog and only test on x86 2022-11-04 23:34:36 +01:00
03e9fb431f put flows back together 2022-11-04 22:10:19 +01:00
b84df2866b build matrix 2022-11-02 13:36:56 +01:00
3d4fef7c23 try with matrix build 2022-11-02 13:34:41 +01:00
9d787008a4 also build docker when testing 2022-11-02 13:29:19 +01:00
687f26bb40 name the workflow 2022-11-02 13:26:57 +01:00
371a869800 use nightly cargo with sparse registry 2022-11-02 13:24:30 +01:00
321c303a8a changelog 2022-10-29 19:45:14 +02:00
2f176d84e9 wrong docker compose 2022-10-29 19:43:59 +02:00
67d4f09bd7 #62 (#63)
* #62 add theme options for title and favicon

* docs

* version bump
2022-10-27 17:26:56 +02:00
c40f009523 Update README.md 2022-10-24 16:35:54 +02:00
026f8c69d7 add size limit to redis 2022-10-24 16:11:50 +02:00
cacb808117 restructuring (#56)
* restructuring

* pin svelte kit version & parallel execution

* update svelte kit

* correct test result assets

* add timeout

* correct locale path

* simplify crypto

* fix for #58

* add verbosity flag

* disable flaky test
2022-10-07 21:28:25 +02:00
2d573edcac change link 2022-09-12 14:24:05 +02:00
4287cd429d security reporting 2022-09-10 13:13:09 +02:00
024dfeeeb7 add url spec 2022-07-26 23:48:53 +02:00
f24bcba20b remove ununsed 2022-07-26 15:49:12 +02:00
1d95edc455 readme 2022-07-26 15:49:06 +02:00
hash070
ec24ab3edd Update CN README translate (#47) 2022-07-21 11:32:38 +02:00
a552e4d766 Toasts (#45)
* locales

* add toasts and update deps

* changelog

* lock file
2022-07-20 10:41:37 +02:00
c3b1772728 Testing (#44)
* testing

* try playwrigth

* testing

* add pr support

* not on each commit

* add test ids

* make backend more configuratble

* 2.0.2

* spec
2022-07-19 21:55:05 +02:00
786878a3e4 Testing (#41)
* testing

* try playwrigth

* testing

* add pr support

* not on each commit
2022-07-19 14:12:51 +02:00
a5d98b76bd 2.0.1 (#40)
* locale from lokalise

* version bump

* update dependencies

* show size with overhead

* use base64 instead of hex and refactor a bit

* changelog & readme

* size limit

* locale

* add sync for svelte

* refarcor create & add loading animation

* changelog
2022-07-19 10:27:23 +02:00
9590c9b567 2 (#38)
* use redis

* update frontend and switch sanitize library

* changelog

* theming

* docker image

* documentation

* changelog

* clear up limit sizes

* version bump

* version bump
2022-07-16 14:16:54 +02:00
hash070
0913a8ad0c created and made a Chinese translation JSON file at ./frontend/locales/cn.json (#37) 2022-07-12 14:30:47 +02:00
d13c712e95 Update README.md 2022-07-08 10:45:40 +02:00
6230d2dbd0 Merge pull request #36 from Hash070/cn-translate
Completed the Chinese translation of README
2022-07-08 10:44:17 +02:00
hash070
dbfb383c73 Completed the Chinese translation of README 2022-07-08 16:18:12 +08:00
a257d2cefb use env var for startup script 2022-06-07 12:41:20 +02:00
35ba25ba9e changelog 2022-06-07 12:29:58 +02:00
724dca0e69 update deps 2022-06-07 12:29:54 +02:00
9029f72a02 improve docker build and wait for memcached 2022-06-07 12:29:40 +02:00
1d55d7f2d2 update deps 2022-06-07 12:29:16 +02:00
d09bb4e0c6 update deps 2022-06-07 12:29:11 +02:00
53c7c9d9e2 1.5.1 2022-05-15 12:13:03 +02:00
df9c60c29e 1.5.0 2022-05-14 16:57:28 +02:00
f29b6b23f0 Update README.md 2022-04-12 09:34:38 +02:00
cc88fa6763 readme 2022-04-12 09:12:40 +02:00
19022e7cb5 formatting 2022-03-12 14:07:33 +01:00
44f43dbc2c scratch docs 2022-03-06 12:27:08 +01:00
45f6f3af32 readme from scratch 2022-03-06 12:16:19 +01:00
9bd544f0d5 add env vars to readme 2022-03-06 12:01:54 +01:00
a315e58284 Merge pull request #26 from cupcakearmy/1.4.1
fallback route & dep updates
2022-03-05 12:47:55 +01:00
d576b71bc5 fallback route & dep updates 2022-03-05 12:47:11 +01:00
e02f7f59c6 changelog 2022-03-02 16:59:25 +01:00
e8c6467faa dont' allow empty notes 2022-03-02 16:55:10 +01:00
43f67c795d don't remove already selected files 2022-03-02 16:55:04 +01:00
83f0902291 lokalise 2022-03-02 16:32:36 +01:00
11a6621bd7 script to download locales 2022-03-02 16:15:25 +01:00
36fa451249 enforce limits 2022-03-01 16:16:02 +01:00
d112eba8fe add config for max views, expiration and advanced 2022-03-01 15:24:17 +01:00
ef39f9ec0b cleanup 2022-03-01 02:04:43 +01:00
8517c20e6c remove unused code 2022-03-01 02:01:57 +01:00
728ad56b33 enforce strict typescript 2022-03-01 02:00:01 +01:00
f185ccee03 add svelte check 2022-03-01 01:52:20 +01:00
284bbcbae2 better typings 2022-03-01 01:52:09 +01:00
7eba454f1b visual improvements 2022-03-01 01:52:04 +01:00
dcd9efaeba use native icons 2022-03-01 01:51:43 +01:00
f13bcbaf3f update robots 2022-03-01 01:51:21 +01:00
8e7e0414a6 add copy to clipboard note 2022-03-01 01:16:31 +01:00
229c8d8368 update actix to version 4 2022-03-01 00:53:47 +01:00
1adf87b884 set memcached size 2022-03-01 00:53:34 +01:00
a061b540b1 make top margin smaller 2022-01-21 17:46:13 +01:00
824603ff4a add locales 2022-01-16 14:39:45 +01:00
539d99d35f version bump 2022-01-16 14:04:49 +01:00
716034660c translate the app 2022-01-16 14:02:53 +01:00
bab65bcdad bug due to dep update 2022-01-03 18:16:54 +01:00
a0732a4593 move folder 2022-01-02 23:46:08 +01:00
835f7df0f6 update dep 2022-01-02 18:43:31 +01:00
2def365cae quality of life improvemnts 2021-12-30 22:36:28 +01:00
c8b2539414 add version in about page 2021-12-22 15:20:30 +01:00
c8a25eb9bf release date 2021-12-22 14:57:58 +01:00
15bceb1715 add env 2021-12-22 14:56:33 +01:00
8acc4108ae Update README.md 2021-12-22 14:54:02 +01:00
0f708f53c0 changelog 2021-12-22 14:48:48 +01:00
8d03ad8e15 changelog 2021-12-22 14:47:43 +01:00
33829768eb time based fix 2021-12-22 14:46:06 +01:00
8cee6579e2 file upload 2021-12-22 13:10:08 +01:00
8eeb2a8de7 forgot missing packages 2021-12-21 10:35:02 +01:00
e4ce767444 add support for files 2021-12-21 00:15:04 +01:00
00fd514da5 feedback on to big error 2021-12-20 18:22:10 +01:00
ba38d2b819 proxy for cors 2021-12-20 18:14:59 +01:00
d0f83e6148 changelog 2021-12-20 17:54:17 +01:00
a040ad469e remove println 2021-12-20 17:54:09 +01:00
0c01866344 Merge pull request #15 from cupcakearmy/size-limit
Size limit
2021-12-20 17:45:05 +01:00
048c5198a2 Merge remote-tracking branch 'origin/main' into size-limit 2021-12-20 17:44:53 +01:00
f606916d97 Merge pull request #14 from cupcakearmy/pnpm
Pnpm
2021-12-20 17:43:11 +01:00
aea85c3b73 env docs 2021-12-20 17:42:35 +01:00
5f904b3971 cargo file 2021-12-20 17:42:30 +01:00
ac5d52a010 middleware to handle json payloads 2021-12-20 17:42:16 +01:00
8644a937d0 remove hardcoded limit 2021-12-20 17:42:08 +01:00
a0ebb97bc5 pnpm 2021-12-20 17:41:03 +01:00
19cd9b8507 examples on deployment 2021-12-16 13:54:15 +01:00
fe653e91c8 pnpm 2021-12-16 13:40:50 +01:00
a78ec72687 readme 2021-11-23 15:43:57 +01:00
a462bed948 1.2.0 2021-11-11 13:37:21 +01:00
325518ba15 Update README.md 2021-09-01 11:10:02 +02:00
4b80912727 Merge pull request #11 from stiivo/patch-1
Update README.md
2021-09-01 10:58:13 +02:00
Steven Günther
c78ad636c3 Update README.md
typo
2021-09-01 10:19:15 +02:00
4fe7833977 Update README.md 2021-06-01 12:23:08 +02:00
24f9aeb229 arm images 2021-05-19 11:29:33 +02:00
976413e11b limit min height 2021-05-17 09:45:01 +02:00
2480d875b4 about page 2021-05-16 12:47:52 +02:00
5dff12ea70 use hash instead of path for key 2021-05-16 11:16:25 +02:00
e332dc63e8 on push 2021-05-14 15:09:55 +02:00
a18e9bcc88 Merge pull request #7 from cupcakearmy/testing
use docker-compose
2021-05-14 14:48:56 +02:00
4b43edf54a use docker-compose 2021-05-14 14:48:34 +02:00
e3aa2dd5ff Update test.yml 2021-05-10 10:13:05 +02:00
98a03c25e6 Merge pull request #6 from cupcakearmy/testing
Testing
2021-05-10 10:11:59 +02:00
7f618e7e45 ci 2021-05-10 10:11:31 +02:00
84a7be4549 config 2021-05-10 10:04:19 +02:00
b2bad5f64c cypress runner 2021-05-10 09:58:21 +02:00
41f55c0920 test ids 2021-05-10 09:58:13 +02:00
edbf8a8ecf changelog 2021-05-08 21:47:13 +02:00
4852804581 time bug 2021-05-08 21:47:08 +02:00
22b1c35b3e loading state 2021-05-08 21:46:52 +02:00
d1e9ffd89b up the iterations 2021-05-08 21:46:43 +02:00
9c675ba48c notes about availability 2021-05-08 21:46:33 +02:00
ef3d3d5bde changelog 2021-05-08 10:34:18 +02:00
7e835af3f2 changelog 2021-05-08 10:17:20 +02:00
f153102978 bug 2021-05-08 10:16:05 +02:00
5944c784ba changelog 2021-05-07 14:12:23 +02:00
2aa186450a one less dependency 2021-05-07 14:09:26 +02:00
70b53106ea use svg as text 2021-05-07 12:14:07 +02:00
b958189b0c update dependencies 2021-05-07 11:57:28 +02:00
acd488aab0 typos 2021-05-05 10:52:10 +02:00
5378b7a820 changelog 2021-05-05 10:44:38 +02:00
1bb5d3ecb0 ios appearance and theme override 2021-05-05 10:42:37 +02:00
0d79e9c85e remove arm builds for now 2021-05-05 09:57:30 +02:00
70382a63ed slash at the end 2021-05-04 19:45:19 +02:00
b3886cc6fc add depends on 2021-05-04 19:30:25 +02:00
cfe525f274 arm images 2021-05-03 18:53:45 +02:00
96e8ec4b67 Update README.md 2021-05-03 12:38:51 +02:00
7a3397f978 always encrypt content 2021-05-03 12:21:51 +02:00
dc212d7441 docs 2021-05-03 12:21:44 +02:00
6e04594b4d changelog 2021-05-03 11:26:04 +02:00
b6834aa829 logos 2021-05-03 11:10:53 +02:00
093a6c5c2b comments 2021-05-02 19:03:16 +02:00
407d220552 changelog 2021-05-02 18:52:35 +02:00
bf8593080b from scratch 2021-05-02 18:50:43 +02:00
d63d529d2b Merge branch 'main' of https://github.com/cupcakearmy/cryptgeon 2021-05-02 18:28:48 +02:00
c4f545d1e8 styling 2021-05-02 18:28:46 +02:00
b94588ead4 Delete FUNDING.yml 2021-05-02 17:19:55 +02:00
96657b89d2 Create FUNDING.yml 2021-05-02 17:19:45 +02:00
d05b090252 Create FUNDING.yml 2021-05-02 17:18:48 +02:00
d262a37db6 logo 2021-05-02 17:15:28 +02:00
a66e8033df logo 2021-05-02 17:14:03 +02:00
95ba8d1fed typo 2021-05-02 17:12:12 +02:00
f93be44449 Merge branch 'main' of https://github.com/cupcakearmy/cryptgeon 2021-05-02 17:11:26 +02:00
f73aa1bcf8 screens 2021-05-02 17:11:24 +02:00
6e25bb697f Update README.md 2021-05-02 16:57:56 +02:00
5a105a7b29 changelog 2021-05-02 16:25:11 +02:00
c8dfeaec12 Merge branch 'main' of https://github.com/cupcakearmy/cryptgeon 2021-05-02 16:24:33 +02:00
bf2c95bfb6 move svg from lfs to repo 2021-05-02 16:24:31 +02:00
4aef5a1b04 checkout with lfs 2021-05-02 16:20:08 +02:00
7520b6b1da Update README.md 2021-05-02 16:02:57 +02:00
174 changed files with 11671 additions and 5734 deletions

View File

@@ -1 +1,15 @@
target
*
!/packages
!/package.json
!/pnpm-lock.yaml
!/pnpm-workspace.yaml
**/target
**/node_modules
**/dist
**/bin
**/*.tsbuildinfo
**/build
**/.svelte
**/.svelte-kit

2
.env.dev Normal file
View File

@@ -0,0 +1,2 @@
SIZE_LIMIT=10MiB
VERBOSITY=debug

2
.gitattributes vendored
View File

@@ -1,2 +1,2 @@
*.afdesign filter=lfs diff=lfs merge=lfs -text
*.svg filter=lfs diff=lfs merge=lfs -text
test/assets/** filter=lfs diff=lfs merge=lfs -text

12
.github/FUNDING.yml vendored Normal file
View File

@@ -0,0 +1,12 @@
# These are supported funding model platforms
github: cupcakearmy
patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
otechie: # Replace with a single Otechie username
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']

BIN
.github/lokalise.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

View File

@@ -1,37 +0,0 @@
name: ci
on:
workflow_dispatch:
push:
tags:
- "v*.*.*"
jobs:
docker:
runs-on: ubuntu-latest
steps:
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
- name: Docker Labels
id: meta
uses: crazy-max/ghaction-docker-meta@v2
with:
images: cupcakearmy/cryptgeon
tags: |
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
- name: Login to DockerHub
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push
id: docker_build
uses: docker/build-push-action@v2
with:
push: true
tags: ${{ steps.meta.outputs.tags }}
- name: Image digest
run: echo ${{ steps.docker_build.outputs.digest }}

58
.github/workflows/release.yml vendored Normal file
View File

@@ -0,0 +1,58 @@
name: Publish
on:
workflow_dispatch:
push:
tags:
- 'v*.*.*'
jobs:
cli:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: pnpm/action-setup@v2
- uses: actions/setup-node@v3
with:
cache: 'pnpm'
node-version-file: '.nvmrc'
registry-url: 'https://registry.npmjs.org'
- run: |
pnpm install --frozen-lockfile
pnpm run build
- run: npm publish
working-directory: ./packages/cli
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
docker:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: docker/setup-qemu-action@v2
- uses: docker/setup-buildx-action@v2
with:
install: true
- name: Docker Labels
id: meta
uses: docker/metadata-action@v4
with:
images: cupcakearmy/cryptgeon
tags: |
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
- name: Login to DockerHub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v4
with:
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}

42
.github/workflows/test.yaml vendored Normal file
View File

@@ -0,0 +1,42 @@
name: Test
on:
push:
branches:
- main
pull_request:
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
# Node
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
cache: 'pnpm'
node-version-file: '.nvmrc'
# Docker
- uses: docker/setup-qemu-action@v3
- uses: docker/setup-buildx-action@v3
with:
install: true
- name: Prepare
run: |
pnpm install
pnpm exec playwright install --with-deps
pnpm run test:prepare
- name: Run your tests
run: pnpm test
- uses: actions/upload-artifact@v4
if: ${{ !cancelled() }}
with:
name: playwright-report
path: test-results/
retention-days: 7

19
.gitignore vendored
View File

@@ -1,10 +1,11 @@
# Backend
/target
# Client
.DS_Store
.env
*.tsbuildinfo
node_modules
/.svelte
/build
/functions
dist
bin
target
# Testing
test-results
tmp

1
.nvmrc Normal file
View File

@@ -0,0 +1 @@
v22.14.0

View File

@@ -1,6 +0,0 @@
{
"cSpell.words": [
"ciphertext",
"cryptgeon"
]
}

View File

@@ -5,16 +5,325 @@ 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/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [2.4.0] - 2023-11-01
### Changed
- Removed HTML sanitation, display the original message as string
- Links are now displayed under the note in a separate section
## [2.3.1] - 2023-06-23
### Added
- #92: Endpoint (`/api/live/`) for checking health status.
## [2.3.0] - 2023-05-30
### Added
- New CLI 🎉.
- Russian language.
- Option for reducing note id size (`ID_LENGTH`).
### Changed
- Moved to monorepo.
### Changed
- Default port is now 8000, not 5000.
- Moved to generic encryption library `occulto`.
### Fixed
- Bad chinese language code.
### Security
- Updated dependencies.
## [2.1.0] - 2023-01-04
### Added
- QR Code to more easily copy and share links.
## [2.0.7] - 2022-12-26
### Changed
- Svelte Kit now stable 🎉
## [2.0.6] - 2022-11-12
### Fixed
- #66 Set minimum a view.
### Security
- Updated dependencies.
## [2.0.5] - 2022-11-04
### Fixed
- Docker build pipeline.
## [2.0.4] - 2022-10-29
### Added
- `THEME_PAGE_TITLE`.
- `THEME_FAVICON`.
## [2.0.3] - 2022-10-07
### Added
- Flag for verbosity.
### Fixed
- #58 Fixed bug in the max views frontend form.
## [2.0.2] - 2022-07-20
### Added
- Toasts for events.
- E2E Tests.
- Make backend more configurable.
## [2.0.1] - 2022-07-18
### Added
- Max file size on the client now.
- Loading information.
### Changed
- Changed encoding from hex to base64.
- Chinese language code.
- Notable speed improvements for big files.
## [2.0.0] - 2022-07-16
### Added
- Theming for logo and description text.
### Changed
- Moved to redis.
- New html sanitizing library.
## [2.0.0-rc.0] - 2022-07-15
### Added
- Theming for logo and description text.
### Changed
- Moved to redis.
- New html sanitizing library.
## [1.5.3] - 2022-06-07
### Changed
- Use the value from the `MEMCACHE` env variable in startup script.
## [1.5.2] - 2022-06-07
### Added
- Wait for script for memecached.
### Security
- Updated dependencies.
## [1.5.1] - 2022-05-15
### Fixed
- Remove double note content.
## [1.5.0] - 2022-05-14
### Added
- Links in notes are not highlighted and can be directly clicked #30.
## [1.4.1] - 2022-03-05
### Fixed
- Router in prod build.
## [1.4.0] - 2022-03-02
### Added
- Support for multiple languages.
- Select multiple files without removing already selected ones.
- Tooltip for copy action.
- Configure maximum views, expiration and advanced options for the server.
### Changed
- Use native SVGs instead of images.
- Update robots.txt file to allow only root.
- Stronger frontend types.
## [1.3.3] - 2022-01-03
### Fixed
- Bug fix due to dependency update.
## [1.3.2] - 2022-01-02
### Changed
- Dependencies updates.
- Folder structure.
## [1.3.1] - 2021-12-30
### Added
- Short explanation in the home page.
### Changed
- Explanation in about & readme.
- Shorten server ids from 512 to 256bit.
## [1.3.0] - 2021-12-22
### Added
- Option to set a custom size limit.
- Options to share files.
### Changed
- Don't delete note if time is not expired yet
- Use pnpm instead of npm.
## [1.2.0] - 2021-11-11
### Changed
- Switch to pnpm.
### Security
- Dependencies updated.
## [1.1.1] - 2021-05-17
### Fixed
- Height on big displays.
- About page.
## [1.1.0] - 2021-05-16
### Security
- Using hash `#` instead of path.
## [1.0.11] - 2021-05-08
### Added
- loading text.
- description for created notes about availability.
### Changed
- iterations from 100 to 100k.
### Fixed
- time based view bug.
## [1.0.10] - 2021-05-08
### Fixed
- API endpoint was not reachable.
## [1.0.9] - 2021-05-07
## Changed
- Removed a dependency.
## [1.0.8] - 2021-05-05
### Added
- Manual theme override option.
### Fixed
- Removed Arm builds for now.
- iOS style bugs.
## [1.0.7] - 2021-05-04
### Added
- Arm images.
## [1.0.6] - 2021-05-04
### Added
- Always use encryption with random passwords included links.
## [1.0.5] - 2021-05-03
### Fixed
- Typos.
## [1.0.4] - 2021-05-02
### Added
- From scratch docker image.
## [1.0.3] - 2021-05-02
### Fixed
- Higher default text area.
- Mobile touchups.
## [1.0.2] - 2021-05-02
### Fixed
- SVG Icons.
## [1.0.1] - 2021-05-02
### Added
- Dark mode support
- Dark mode support.
### Fixed
- Don't reload data on wrong password
- Don't reload data on wrong password.
## [1.0.0] - 2021-05-02
Initial release
Initial release.

2030
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,21 +0,0 @@
[package]
name = "cryptgeon"
version = "1.0.0"
authors = ["cupcakearmy <hi@nicco.io>"]
edition = "2018"
[[bin]]
name = "cryptgeon"
path = "src/main.rs"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
actix-web = "3"
actix-files = "0.5"
serde = "1"
serde_json = "1"
lazy_static = "1"
ring = "0.16"
bs62 = "0.1"
memcache = "0.15"

View File

@@ -0,0 +1,614 @@
{
"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"
}
]
}

View File

@@ -1,26 +1,30 @@
FROM node:16-alpine as CLIENT
# FRONTEND
FROM node:22-alpine as client
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
RUN corepack enable
WORKDIR /tmp
COPY ./client .
COPY . .
RUN pnpm install --frozen-lockfile
RUN pnpm run build
RUN npm ci
RUN npm run build
FROM rust:1.51-alpine as RUST
# BACKEND
FROM rust:1.85-alpine as backend
WORKDIR /tmp
RUN apk add libc-dev openssl-dev alpine-sdk
COPY ./Cargo* .
COPY ./src ./src
RUN apk add --no-cache libc-dev openssl-dev alpine-sdk
COPY ./packages/backend ./
RUN RUSTFLAGS="-Ctarget-feature=-crt-static" cargo build --release
RUN cargo build --release
FROM alpine
# RUNNER
FROM alpine:3.19
WORKDIR /app
COPY --from=RUST /tmp/target/release/cryptgeon .
COPY --from=CLIENT /tmp/build ./client/build
ENV MEMCACHE=memcached:11211
ENTRYPOINT [ "/app/cryptgeon" ]
RUN apk add --no-cache curl libgcc
COPY --from=backend /tmp/target/release/cryptgeon .
COPY --from=client /tmp/packages/frontend/build ./frontend
ENV FRONTEND_PATH="./frontend"
ENV REDIS="redis://redis/"
EXPOSE 8000
ENTRYPOINT [ "/app/cryptgeon" ]

209
README.md
View File

@@ -1,63 +1,226 @@
<p align="center">
<img src="./design/github.svg">
<img src="./design/Github.png" alt="logo">
</p>
<a href="https://discord.gg/nuby6RnxZt">
<img alt="discord" src="https://img.shields.io/discord/252403122348097536?style=for-the-badge" />
<img alt="docker pulls" src="https://img.shields.io/docker/pulls/cupcakearmy/cryptgeon?style=for-the-badge" />
<img alt="Docker image size badge" src="https://img.shields.io/docker/image-size/cupcakearmy/cryptgeon?style=for-the-badge" />
<img alt="Latest version" src="https://img.shields.io/github/v/release/cupcakearmy/cryptgeon?style=for-the-badge" />
</a>
<br/><br/>
<a href="https://www.producthunt.com/posts/cryptgeon?utm_source=badge-featured&utm_medium=badge&utm_souce=badge-cryptgeon" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=295189&theme=light" alt="Cryptgeon - Securely share self-destructing notes | Product Hunt" height="50" /></a>
<a href=""><img src="./.github/lokalise.png" height="50">
<a title="Install cryptgeon Raycast Extension" href="https://www.raycast.com/cupcakearmy/cryptgeon"><img src="https://www.raycast.com/cupcakearmy/cryptgeon/install_button@2x.png?v=1.1" height="64" alt="" style="height: 64px;"></a>
<br/><br/>
EN | [简体中文](README_zh-CN.md) | [ES](README_ES.md)
## About?
_cryptgeon_ is an secure, open source sharing note service inspired by [_PrivNote_](https://privnote.com)
_cryptgeon_ is a secure, open source sharing note or file service inspired by [_PrivNote_](https://privnote.com).
It includes a server, a web page and a CLI client.
## Demo
> 🌍 If you want to translate the project feel free to reach out to me.
>
> Thanks to [Lokalise](https://lokalise.com/) for providing free access to their platform.
Check out the demo and see for yourself https://cryptgeon.nicco.io.
## Live Service / Demo
### Web
Check out the live service / demo and see for yourself [cryptgeon.org](https://cryptgeon.org)
### CLI
```
npx cryptgeon send text "This is a secret note"
```
For more documentation about the CLI see the [readme](./packages/cli/README.md).
### Raycast Extension
There is an [official Raycast extension](https://www.raycast.com/cupcakearmy/cryptgeon).
<a title="Install cryptgeon Raycast Extension" href="https://www.raycast.com/cupcakearmy/cryptgeon"><img src="https://www.raycast.com/cupcakearmy/cryptgeon/install_button@2x.png?v=1.1" height="64" alt="" style="height: 64px;"></a>
## Features
- view and time constrains
- send text or files
- server cannot decrypt contents due to client side encryption
- view or time constraints
- in memory, no persistence
- in browser encryption → server cannot decrypt contents
- obligatory dark mode support
## How does it work?
each note has a 512bit generated <i>id</i> that is used to retrieve the note. data is stored in memory and never persisted to disk.
each note has a generated <code>id (256bit)</code> and <code>key 256(bit)</code>. The
<code>id</code>
is used to save & retrieve the note. the note is then encrypted with aes in gcm mode on the
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
of the notes even if it tried to.
> View counts are guaranteed with one running instance of cryptgeon. Multiple instances connected to the same Redis instance can run into race conditions, where a note might be retrieved more than the view count allows.
## Screenshot
![screenshot](./design/screen.jpg)
![screenshot](./design/Screens.png)
## Environment Variables
| Variable | Default | Description |
| ----------------------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `REDIS` | `redis://redis/` | Redis URL 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. |
| `MAX_VIEWS` | `100` | Maximal number of views. |
| `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_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_. |
| `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_IMAGE` | `""` | Custom image for replacing the logo. Must be publicly reachable |
| `THEME_TEXT` | `""` | Custom text for replacing the description below the logo |
| `THEME_PAGE_TITLE` | `""` | Custom text the page title |
| `THEME_FAVICON` | `""` | Custom url for the favicon. Must be publicly reachable |
| `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`. |
| `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
Docker is the easiest way.
> `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.
### Docker
Docker is the easiest way. There is the [official image here](https://hub.docker.com/r/cupcakearmy/cryptgeon).
```yaml
# docker-compose.yml
version: '3.7'
version: '3.8'
services:
memcached:
image: memcached:1-alpine
entrypoint: memcached -m 128 # Limit to 128 MB Ram, customize at free will.
redis:
image: redis:7-alpine
# This is required to stay in RAM only.
command: redis-server --save "" --appendonly no
# Set a size limit. See link below on how to customise.
# https://redis.io/docs/latest/operate/rs/databases/memory-performance/eviction-policy/
# --maxmemory 1gb --maxmemory-policy allkeys-lrulpine
# This prevents the creation of an anonymous volume.
tmpfs:
- /data
app:
image: cupcakearmy/cryptgeon:latest
depends_on:
- redis
environment:
# Size limit for a single note.
SIZE_LIMIT: 4 MiB
ports:
- 80:5000
- 80:8000
# Optional health checks
# healthcheck:
# test: ["CMD", "curl", "--fail", "http://127.0.0.1:8000/api/live/"]
# interval: 1m
# timeout: 3s
# retries: 2
# start_period: 5s
```
### NGINX Proxy
See the [examples/nginx](https://github.com/cupcakearmy/cryptgeon/tree/main/examples/nginx) folder. There an example with a simple proxy, and one with https. You need to specify the server names and certificates.
### Traefik 2
See the [examples/traefik](https://github.com/cupcakearmy/cryptgeon/tree/main/examples/traefik) folder.
### Scratch
See the [examples/scratch](https://github.com/cupcakearmy/cryptgeon/tree/main/examples/scratch) folder. There you'll find a guide how to setup a server and install cryptgeon from scratch.
### Synology
There is a [guide](https://mariushosting.com/how-to-install-cryptgeon-on-your-synology-nas/) you can follow.
### YouTube Guides
- English by [Webnestify](https://www.youtube.com/watch?v=XAyD42I7wyI)
- English by [DB Tech](https://www.youtube.com/watch?v=S0jx7wpOfNM) [Previous Video](https://www.youtube.com/watch?v=JhpIatD06vE)
- German by [ApfelCast](https://www.youtube.com/watch?v=84ZMbE9AkHg)
### Written Guides
- French by [zarevskaya](https://belginux.com/installer-cryptgeon-avec-docker/)
- Italian by [@nicfab](https://notes.nicfab.eu/it/posts/cryptgeon/)
- English by [@nicfab](https://notes.nicfab.eu/en/posts/cryptgeon/)
## Development
1. Clone
2. run `npm i` in the root and and client `client/` folders.
3. Run `npm run dev` to start development.
**Requirements**
Running `npm run dev` in the root folder will start the following things
- `pnpm`: `>=9`
- `node`: `>=22`
- `rust`: edition `2021`
- a memcache docker container
- rust backend with hot reload
- client with hot reload
**Install**
```bash
pnpm install
# Also you need cargo watch if you don't already have it installed.
# https://lib.rs/crates/cargo-watch
cargo install cargo-watch
```
**Run**
Make sure you have docker running.
```bash
pnpm run dev
```
Running `pnpm run dev` in the root folder will start the following things:
- redis docker container
- rust backend
- client
- cli
You can see the app under [localhost:3000](http://localhost:3000).
###### Attributions
> There is a Postman collection with some example requests [available in the repo](./Cryptgeon.postman_collection.json)
Icons made by <a href="https://www.freepik.com" title="Freepik">freepik</a> from <a href="https://www.flaticon.com/" title="Flaticon">www.flaticon.com</a>
### Tests
Tests are end to end tests written with Playwright.
```sh
pnpm run test:prepare
# Use the test or test:local script. The local version only runs in one browser for quicker development.
pnpm run test:local
```
## Security
Please refer to the security section [here](./SECURITY.md).
---
_Attributions_
- Test data:
- Text for tests [Nietzsche Ipsum](https://nietzsche-ipsum.com/)
- [AES Paper](https://www.cs.miami.edu/home/burt/learning/Csc688.012/rijndael/rijndael_doc_V2.pdf)
- [Unsplash Pictures](https://unsplash.com/)
- Loading animation by [Nikhil Krishnan](https://codepen.io/nikhil8krishnan/pen/rVoXJa)
- Icons made by <a href="https://www.freepik.com" title="Freepik">freepik</a> from <a href="https://www.flaticon.com/" title="Flaticon">www.flaticon.com</a>

205
README_ES.md Normal file
View File

@@ -0,0 +1,205 @@
<p align="center">
<img src="./design/Github.png" alt="logo">
</p>
<a href="https://discord.gg/nuby6RnxZt">
<img alt="discord" src="https://img.shields.io/discord/252403122348097536?style=for-the-badge" />
<img alt="docker pulls" src="https://img.shields.io/docker/pulls/cupcakearmy/cryptgeon?style=for-the-badge" />
<img alt="Docker image size badge" src="https://img.shields.io/docker/image-size/cupcakearmy/cryptgeon?style=for-the-badge" />
<img alt="Latest version" src="https://img.shields.io/github/v/release/cupcakearmy/cryptgeon?style=for-the-badge" />
</a>
<br/><br/>
<a href="https://www.producthunt.com/posts/cryptgeon?utm_source=badge-featured&utm_medium=badge&utm_souce=badge-cryptgeon" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=295189&theme=light" alt="Cryptgeon - Securely share self-destructing notes | Product Hunt" height="50" /></a>
<a href=""><img src="./.github/lokalise.png" height="50">
<br/><br/>
[EN](README.md) | [简体中文](README_zh-CN.md) | ES
## Acerca de
_cryptgeon_ es un servicio seguro y de código abierto para compartir notas o archivos inspirado en [_PrivNote_](https://privnote.com).
Incluye un servidor, una página web y una interfaz de línea de comandos (CLI, por sus siglas en inglés).
> 🌍 Si quieres traducir este proyecto no dudes en ponerte en contacto conmigo.
>
> Gracias a [Lokalise](https://lokalise.com/) por darnos acceso gratis a su plataforma.
## Demo
### Web
Prueba la demo y experimenta por ti mismo [cryptgeon.org](https://cryptgeon.org)
### CLI
```
npx cryptgeon send text "Esto es una nota secreta"
```
Puedes revisar la documentación sobre el CLI en este [readme](./packages/cli/README.md).
## Características
- enviar texto o archivos
- el servidor no puede desencriptar el contenido debido a que la encriptación se hace del lado del cliente
- restriccion de vistas o de tiempo
- en memoria, sin persistencia
- compatibilidad obligatoria con el modo oscuro
## ¿Cómo funciona?
Se genera una <code>id (256bit)</code> y una <code>llave 256(bit)</code> para cada nota. La
<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.
## Capturas de pantalla
![screenshot](./design/Screens.png)
## Variables de entorno
| 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) |
| `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. |
| `MAX_VIEWS` | `100` | Número máximo de vistas. |
| `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. |
| `ID_LENGTH` | `32` | Establece el tamaño en bytes de la `id` de la nota. Por defecto es de `32` bytes. Esto es util para reducir el tamaño del link. _Esta configuración no afecta el nivel de encriptación_. |
| `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_IMAGE` | `""` | Imagen personalizada para reemplazar el logo. Debe ser accesible públicamente. |
| `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. |
## Despliegue
> Se requiere `https` de lo contrario el navegador no soportará las funciones de encriptacón.
> Hay un endpoint para verificar el estado, lo encontramos en `/api/health/`. Regresa un código 200 o 503.
### Docker
Docker es la manera más fácil. Aquí encontramos [la imágen oficial](https://hub.docker.com/r/cupcakearmy/cryptgeon).
```yaml
# docker-compose.yml
version: '3.8'
services:
redis:
image: redis:7-alpine
# This is required to stay in RAM only.
command: redis-server --save "" --appendonly no
# Set a size limit. See link below on how to customise.
# https://redis.io/docs/latest/operate/rs/databases/memory-performance/eviction-policy/
# --maxmemory 1gb --maxmemory-policy allkeys-lrulpine
# This prevents the creation of an anonymous volume.
tmpfs:
- /data
app:
image: cupcakearmy/cryptgeon:latest
depends_on:
- redis
environment:
# Size limit for a single note.
SIZE_LIMIT: 4 MiB
ports:
- 80:8000
# Optional health checks
# healthcheck:
# test: ["CMD", "curl", "--fail", "http://127.0.0.1:8000/api/live/"]
# interval: 1m
# timeout: 3s
# retries: 2
# start_period: 5s
```
### NGINX Proxy
Ver la carpeta de [ejemplo/nginx](https://github.com/cupcakearmy/cryptgeon/tree/main/examples/nginx). Hay un ejemplo con un proxy simple y otro con https. Es necesario que especifiques el nombre del servidor y los certificados.
### Traefik 2
Ver la carpeta de [ejemplo/traefik](https://github.com/cupcakearmy/cryptgeon/tree/main/examples/traefik).
### Scratch
Ver la carpeta de [ejemplo/scratch](https://github.com/cupcakearmy/cryptgeon/tree/main/examples/scratch). Ahí encontrarás una guía de cómo configurar el servidor e instalar cryptgeon desde cero.
### Synology
Hay una [guía](https://mariushosting.com/how-to-install-cryptgeon-on-your-synology-nas/) (en inglés) que puedes seguir.
### Guías en Youtube
- En inglés, por [Webnestify](https://www.youtube.com/watch?v=XAyD42I7wyI)
- En inglés, por [DB Tech](https://www.youtube.com/watch?v=S0jx7wpOfNM) [Previous Video](https://www.youtube.com/watch?v=JhpIatD06vE)
- En alemán, por [ApfelCast](https://www.youtube.com/watch?v=84ZMbE9AkHg)
## Desarrollo
**Requisitos**
- `pnpm`: `>=6`
- `node`: `>=18`
- `rust`: edition `2021`
**Instalación**
```bash
pnpm install
# También necesitas cargo-watch, si no lo tienes instalado.
# https://lib.rs/crates/cargo-watch
cargo install cargo-watch
```
**Ejecutar**
Asegurate de que docker se esté ejecutando.
```bash
pnpm run dev
```
Ejecutando `pnpm run dev` en la carpeta raíz iniciará lo siguiente:
- redis docker container
- rust backend
- client
- cli
Puedes ver la app en [localhost:3000](http://localhost:3000).
> Existe una colección de Postman con algunas peticiones de ejemplo [disponible en el repo](./Cryptgeon.postman_collection.json)
### Tests
Los tests son end-to-end tests escritos con Playwright.
```sh
pnpm run test:prepare
# Usa el script test o test:local. La versión local solo corre en el navegador para acelerar el desarrollo.
pnpm run test:local
```
## Seguridad
Por favor dirigite a la sección de seguridad [aquí](./SECURITY.md).
---
_Atribuciones_
- Datos del Test:
- Texto para los tests [Nietzsche Ipsum](https://nietzsche-ipsum.com/)
- [AES Paper](https://www.cs.miami.edu/home/burt/learning/Csc688.012/rijndael/rijndael_doc_V2.pdf)
- [Unsplash Imágenes](https://unsplash.com/)
- Animación de carga por [Nikhil Krishnan](https://codepen.io/nikhil8krishnan/pen/rVoXJa)
- Iconos hechos por <a href="https://www.freepik.com" title="Freepik">freepik</a> de <a href="https://www.flaticon.com/" title="Flaticon">www.flaticon.com</a>

199
README_zh-CN.md Normal file
View File

@@ -0,0 +1,199 @@
<p align="center">
<img src="./design/Github_zh-CN.png" alt="logo">
</p>
<a href="https://discord.gg/nuby6RnxZt">
<img alt="discord" src="https://img.shields.io/discord/252403122348097536?style=for-the-badge" />
<img alt="docker pulls" src="https://img.shields.io/docker/pulls/cupcakearmy/cryptgeon?style=for-the-badge" />
<img alt="Docker image size badge" src="https://img.shields.io/docker/image-size/cupcakearmy/cryptgeon?style=for-the-badge" />
<img alt="Latest version" src="https://img.shields.io/github/v/release/cupcakearmy/cryptgeon?style=for-the-badge" />
</a>
<br/>
<a href="https://www.producthunt.com/posts/cryptgeon?utm_source=badge-featured&utm_medium=badge&utm_souce=badge-cryptgeon" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=295189&theme=light" alt="Cryptgeon - Securely share self-destructing notes | Product Hunt" height="50" /></a>
<a href=""><img src="./.github/lokalise.png" height="50">
<br/>
[EN](README.md) | 简体中文 | [ES](README_ES.md)
## 关于本项目
_加密鸽_ 是一个受 [_PrivNote_](https://privnote.com)项目启发的安全、开源共享密信和文件共享服务器
> 🌍 如果你想翻译此项目请随时与我联系.
>
> 感谢 [Lokalise](https://lokalise.com/) 提供免费的平台服务支持
## 演示示例
查看加密鸽的在线演示 demo [cryptgeon.org](https://cryptgeon.org)
## 功能
- 服务端无法解密和查看客户端加密的内容
- 查看次数或时间限制,阅后即焚
- 您发送的数据将存放于内存中,不会写入到磁盘中
- 黑暗模式支持
## 加密鸽是如何工作的?
加密鸽会为每条笔记都生成一个独立的 <code>id (256bit)</code><code>key 256(bit)</code>
其中<code>id</code>用于保存和提取密信, 在这之后这封密信将会被客户端使用 AES 算法的 GCM 模式和`key`进行加密然后发送至服务器,数据将会保存在服务器的内存中且永远不会被持久化到硬盘上,服务端永远不会得到密钥并且无法解读密信的内容。
## 屏幕截图
![screenshot](./design/Screens.png)
## 环境变量
| 变量名称 | 默认值 | 描述 |
| ----------------- | ---------------- | --------------------------------------------------------------------------------- |
| `REDIS` | `redis://redis/` | Redis 连接 URL。 |
| `SIZE_LIMIT` | `1 KiB` | 最大请求体(body)限制。有关支持的数值请查看 [字节单位](https://docs.rs/byte-unit/) |
| `MAX_VIEWS` | `100` | 密信最多查看次数限制 |
| ` MAX_EXPIRATION` | `360` | 密信最长过期时间限制(分钟) |
| `ALLOW_ADVANCED` | `true` | 是否允许自定义设置,该项如果设为`false`,则不会显示自定义设置模块 |
| `THEME_IMAGE` | `""` | 自定义 Logo 图片,你在这里填写的的图片链接必须是可以公开访问的。 |
| `THEME_TEXT` | `""` | 自定义在 Logo 下方的文本。 |
## 部署
加密鸽必须使用`https`,否则浏览器可能将不会支援加密鸽的加密算法。
### Docker
Docker 是最简单的部署方式。这里是[官方镜像的地址](https://hub.docker.com/r/cupcakearmy/cryptgeon)。
附:译者的[部署笔记](https://www.hash070.top/archives/cryptgeon-docker-deploy.html)
```yaml
# docker-compose.yml
version: '3.8'
services:
redis:
image: redis:7-alpine
# This is required to stay in RAM only.
command: redis-server --save "" --appendonly no
# Set a size limit. See link below on how to customise.
# https://redis.io/docs/latest/operate/rs/databases/memory-performance/eviction-policy/
# --maxmemory 1gb --maxmemory-policy allkeys-lrulpine
# This prevents the creation of an anonymous volume.
tmpfs:
- /data
app:
image: cupcakearmy/cryptgeon:latest
depends_on:
- redis
environment:
SIZE_LIMIT: 4 MiB
ports:
- 80:8000
```
### NGINX 反向代理
查看 [examples/nginx](https://github.com/cupcakearmy/cryptgeon/tree/main/examples/nginx) 目录。那里有几个示例反代配置文件模板,其中一个是带 https 配置的反代配置模板,你需要指定服务器的名称和证书才能生效。
### Traefik 2
假设:
- 外部 Docker 代理网络 `proxy`
- 证书解析器 `le`
- 一个 https 入站点 `secure`
- 域名 `example.org`
```yaml
version: '3.8'
networks:
proxy:
external: true
services:
redis:
image: redis:7-alpine
# This is required to stay in RAM only.
command: redis-server --save "" --appendonly no
# Set a size limit. See link below on how to customise.
# https://redis.io/docs/latest/operate/rs/databases/memory-performance/eviction-policy/
# --maxmemory 1gb --maxmemory-policy allkeys-lrulpine
# This prevents the creation of an anonymous volume.
tmpfs:
- /data
app:
image: cupcakearmy/cryptgeon:latest
restart: unless-stopped
depends_on:
- redis
networks:
- default
- proxy
labels:
- traefik.enable=true
- traefik.http.routers.cryptgeon.rule=Host(`example.org`)
- traefik.http.routers.cryptgeon.entrypoints=secure
- traefik.http.routers.cryptgeon.tls.certresolver=le
```
## 开发
**环境要求**
- `pnpm`: `>=6`
- `node`: `>=14`
- `rust`: edition `2021`
**安装**
```bash
pnpm install
pnpm --prefix frontend install
# 你还需要安装CargoWatch.
# https://lib.rs/crates/cargo-watch
cargo install cargo-watch
```
**运行**
确保你的 Docker 正在运行
```bash
pnpm run dev
```
在根目录执行 `pnpm run dev` 会开启下列服务:
- 一个 redis docker 容器
- 无热重载的 rust 后端
- 可热重载的客户端
你可以通过 3000 端口进入该应用,即 [localhost:3000](http://localhost:3000).
## 测试
这些测试是用 Playwright 实现的一些端到端测试用例。
```sh
pnpm run test:prepare
docker compose up redis -d
pnpm run test:server
# 在另一个终端中:
# 使用test或者test:local script。为了更快的开发本地版本只会在一个浏览器中运行。
pnpm run test:local
```
###### Attributions
- 测试数据:
- 测试文本 [Nietzsche Ipsum](https://nietzsche-ipsum.com/)
- [AES Paper](https://www.cs.miami.edu/home/burt/learning/Csc688.012/rijndael/rijndael_doc_V2.pdf)
- [Unsplash Pictures](https://unsplash.com/)
- 加载动画由 [Nikhil Krishnan](https://codepen.io/nikhil8krishnan/pen/rVoXJa) 提供
- 图标由来自 <a href="https://www.flaticon.com/" title="Flaticon">www.flaticon.com</a><a href="https://www.freepik.com" title="Freepik">freepik</a> 提供

18
SECURITY.md Normal file
View File

@@ -0,0 +1,18 @@
# Security Policy
## Supported Versions
Please ensure that you are using the latest major version available.
| Version | Supported |
| ------- | --------- |
| 2.x | ✅ |
| < 1.x | |
## Reporting a vulnerability
_cryptgeon_ has a full disclosure vulnerability policy.
Report any bug / vulnerability directly to the [issue tracker](https://github.com/cupcakearmy/cryptgeon/issues).
Please do NOT attempt to report any security vulnerability in this code privately to anybody.
> Shamefully copied of the [ring security section](https://github.com/briansmith/ring#bug-reporting).

View File

@@ -1,5 +0,0 @@
.DS_Store
node_modules
/.svelte
/build
/functions

View File

@@ -1 +0,0 @@
engine-strict=true

View File

@@ -1,38 +0,0 @@
# create-svelte
Everything you need to build a Svelte project, powered by [`create-svelte`](https://github.com/sveltejs/kit/tree/master/packages/create-svelte);
## Creating a project
If you're seeing this, you've probably already done this step. Congrats!
```bash
# create a new project in the current directory
npm init svelte@next
# create a new project in my-app
npm init svelte@next my-app
```
> Note: the `@next` is temporary
## Developing
Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
```bash
npm run dev
# or start the server and open the app in a new browser tab
npm run dev -- --open
```
## Building
Before creating a production version of your app, install an [adapter](https://kit.svelte.dev/docs#adapters) for your target environment. Then:
```bash
npm run build
```
> You can preview the built app with `npm run preview`, regardless of whether you installed an adapter. This should _not_ be used to serve your app in production.

View File

@@ -1,7 +0,0 @@
├─ MIT: 43
├─ MIT*: 2
├─ BSD-3-Clause: 2
├─ ISC: 1
├─ 0BSD: 1
└─ Apache-2.0: 1
1 ├─ MIT: 43
2 ├─ MIT*: 2
3 ├─ BSD-3-Clause: 2
4 ├─ ISC: 1
5 ├─ 0BSD: 1
6 └─ Apache-2.0: 1

1000
client/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,24 +0,0 @@
{
"private": true,
"scripts": {
"dev": "svelte-kit dev",
"build": "svelte-kit build",
"preview": "svelte-kit preview",
"licenses": "npx license-checker --summary > licenses.csv"
},
"devDependencies": {
"@sveltejs/adapter-static": "next",
"@sveltejs/kit": "next",
"svelte": "^3.34.0",
"svelte-preprocess": "^4.0.0",
"tslib": "^2.0.0",
"typescript": "^4.0.0",
"vite": "^2.1.0"
},
"type": "module",
"dependencies": {
"@fontsource/fira-mono": "^4.2.2",
"axios": "^0.21.1",
"copy-to-clipboard": "^3.3.1"
}
}

View File

@@ -1,80 +0,0 @@
@import '@fontsource/fira-mono';
* {
box-sizing: border-box;
}
:root {
font-family: 'Fira Mono', monospace;
--ui-bg-0: #fefefe;
--ui-bg-0-85: #fefefed9;
--ui-bg-1: #eee;
--ui-bg-2: #e2e2e2;
--ui-text-0: #111;
--ui-text-1: #222;
--ui-clr-primary: hsl(186, 65%, 55%);
--ui-clr-error: hsl(357, 77%, 51%);
--ui-anim: all 150ms ease;
}
@media (prefers-color-scheme: dark) {
:root {
--ui-bg-0: #111;
--ui-bg-0-85: #111111d9;
--ui-bg-1: #222;
--ui-bg-2: #282828;
--ui-text-0: #fefefe;
--ui-text-1: #eee;
}
}
.error-text {
color: var(--ui-clr-error);
}
body {
min-height: 100vh;
margin: 0;
background-color: var(--ui-bg-0);
color: var(--ui-text-0);
}
a {
color: inherit;
text-decoration: inherit;
box-sizing: content-box;
border-bottom: 2px solid var(--ui-bg-2);
cursor: pointer;
}
a:active {
border-color: var(--ui-clr-primary);
}
a:hover {
border-color: var(--ui-text-0);
}
input,
textarea,
button {
appearance: none;
transition: var(--ui-anim);
font-family: inherit;
font-size: inherit;
background: inherit;
color: inherit;
}
*:disabled,
*[disabled='true'] {
opacity: 0.5;
}
fieldset {
margin: 0;
padding: 0;
border: none;
}

View File

@@ -1,3 +0,0 @@
/// <reference types="@sveltejs/kit" />
/// <reference types="svelte" />
/// <reference types="vite/client" />

View File

@@ -1,38 +0,0 @@
import axios from 'axios'
import { dev } from '$app/env'
const base = axios.create({ baseURL: dev ? 'http://localhost:5000' : undefined })
export type Note = {
contents: string
password: boolean
views?: number
expiration?: number
}
export type NoteInfo = Pick<Note, 'password'>
export type NotePublic = Pick<Note, 'contents'>
export async function create(note: Note) {
const { data } = await base({
url: '/api/notes',
method: 'post',
data: note,
})
return data as { id: string }
}
export async function get(id: string) {
const { data } = await base({
url: `/api/notes/${id}`,
method: 'delete',
})
return data as NotePublic
}
export async function info(id: string) {
const { data } = await base({
url: `/api/notes/${id}`,
method: 'get',
})
return data as NoteInfo
}

View File

@@ -1,71 +0,0 @@
export class Hex {
static encode(buffer: ArrayBuffer): string {
let s = ''
for (const i of new Uint8Array(buffer)) {
s += i.toString(16).padStart(2, '0')
}
return s
}
static decode(s: string): ArrayBuffer {
const size = s.length / 2
const buffer = new Uint8Array(size)
for (let i = 0; i < size; i++) {
const idx = i * 2
const segment = s.slice(idx, idx + 2)
buffer[i] = parseInt(segment, 16)
}
return buffer
}
}
const ALG = 'AES-GCM'
export function getRandomBytes(size = 16): Uint8Array {
return window.crypto.getRandomValues(new Uint8Array(size))
}
export function getKeyFromString(password: string) {
return window.crypto.subtle.importKey(
'raw',
new TextEncoder().encode(password),
'PBKDF2',
false,
['deriveBits', 'deriveKey']
)
}
export async function getDerivedForKey(key: CryptoKey, salt: ArrayBuffer) {
const iterations = 1_000
return window.crypto.subtle.deriveKey(
{
name: 'PBKDF2',
salt,
iterations,
hash: 'SHA-512',
},
key,
{ name: ALG, length: 256 },
true,
['encrypt', 'decrypt']
)
}
export async function encrypt(plaintext: string, key: CryptoKey) {
const salt = getRandomBytes(16)
const derived = await getDerivedForKey(key, salt)
const iv = getRandomBytes(16)
const encrypted = await window.crypto.subtle.encrypt(
{ name: ALG, iv },
derived,
new TextEncoder().encode(plaintext)
)
return [salt, iv, encrypted].map(Hex.encode).join(':')
}
export async function decrypt(ciphertext: string, key: CryptoKey) {
const [salt, iv, encrypted] = ciphertext.split(':').map(Hex.decode)
const derived = await getDerivedForKey(key, salt)
const plaintext = await window.crypto.subtle.decrypt({ name: ALG, iv }, derived, encrypted)
return new TextDecoder().decode(plaintext)
}

View File

@@ -1,33 +0,0 @@
<script lang="ts">
import { onMount } from 'svelte'
export let icon: string
$: src = `/icons/${icon}.svg`
let html = null
onMount(async () => {
html = await fetch(src).then((res) => res.text())
})
</script>
{#if html === null}
<img on:click {...$$restProps} {src} alt={icon} />
{:else}
<div on:click {...$$restProps}>
{@html html}
</div>
{/if}
<style>
img,
div {
display: inline-block;
contain: strict;
box-sizing: content-box;
}
div > :global(svg) {
display: block;
fill: currentColor;
}
</style>

View File

@@ -1,29 +0,0 @@
<script lang="ts">
export let label: string = ''
export let value: string
</script>
<label>
<small>
{label}
</small>
<textarea {...$$restProps} bind:value />
</label>
<style>
textarea {
width: 100%;
min-height: 8rem;
margin: 0;
padding: 0;
border: 2px solid var(--ui-bg-1);
resize: vertical;
outline: none;
padding: 0.5rem;
}
textarea:hover,
textarea:focus {
border-color: var(--ui-clr-primary);
}
</style>

View File

@@ -1,91 +0,0 @@
<script lang="ts">
import { getRandomBytes, Hex } from '$lib/crypto'
import copyToClipboard from 'copy-to-clipboard'
import Icon from './Icon.svelte'
export let label: string = ''
export let value
export let copy: boolean = false
export let random: boolean = false
const initialType = $$restProps.type
const isPassword = initialType === 'password'
let hidden = true
$: if (isPassword) {
value
$$restProps.type = hidden ? initialType : 'text'
}
function toggle() {
hidden = !hidden
}
function copyFN() {
copyToClipboard(value)
}
function randomFN() {
value = Hex.encode(getRandomBytes(20))
}
</script>
<label>
<small disabled={$$restProps.disabled}>
{label}
</small>
<input bind:value {...$$restProps} />
<div class="icons">
{#if isPassword}
<Icon class="icon" icon={hidden ? 'eye-sharp' : 'eye-off-sharp'} on:click={toggle} />
{/if}
{#if random}
<Icon class="icon" icon="dice-sharp" on:click={randomFN} />
{/if}
{#if copy}
<Icon class="icon" icon="copy-sharp" on:click={copyFN} />
{/if}
</div>
</label>
<style>
label {
position: relative;
display: block;
}
input {
width: 100%;
margin: 0;
border: 2px solid var(--ui-bg-1);
outline: none;
padding: 0.5rem;
height: 2.5rem;
}
input:hover,
input:focus {
border-color: var(--ui-clr-primary);
}
.icons {
border: 1px red;
position: absolute;
right: 0.3rem;
bottom: 0.3rem;
display: flex;
color: var(--ui-clr-primary);
}
.icons > :global(.icon) {
width: 1.5rem;
height: 1.5rem;
background-color: var(--ui-bg-1);
border: 2px solid var(--ui-bg-2);
padding: 1px;
cursor: pointer;
margin-left: 0.25rem;
}
.icons > :global(.icon:hover) {
border-color: var(--ui-clr-primary);
}
</style>

View File

@@ -1,159 +0,0 @@
<script lang="ts">
import type { Note } from '$lib/api'
import { create } from '$lib/api'
import { getKeyFromString, encrypt } from '$lib/crypto'
import Button from '$lib/ui/Button.svelte'
import Switch from '$lib/ui/Switch.svelte'
import TextArea from '$lib/ui/TextArea.svelte'
import TextInput from '$lib/ui/TextInput.svelte'
let note: Note = {
contents: '',
password: false,
views: 1,
expiration: 60,
}
let password: string = ''
let result: { password: string; id: string } | null = null
let advanced = false
let type = false
let message = ''
let loading = false
let error: string | null = null
$: if (!advanced) {
note.views = 1
type = false
}
$: {
let fraction: string
fraction = type ? `${note.expiration} minutes` : `${note.views} views`
message = 'the note will expire and be destroyed after ' + fraction
}
async function submit() {
try {
error = null
loading = true
const data: Note = {
contents: note.contents,
password: !!password,
}
// @ts-ignore
if (type) data.expiration = parseInt(note.expiration)
// @ts-ignore
else data.views = parseInt(note.views)
if (data.password) {
const key = await getKeyFromString(password)
data.contents = await encrypt(data.contents, key)
}
const response = await create(data)
result = {
password: password,
id: response.id,
}
} catch {
error = 'could not create note.'
} finally {
loading = false
}
}
function reset() {
window.location.reload()
}
</script>
{#if result}
{#if result.password}
<TextInput type="password" readonly value={result.password} copy />
<br />
{/if}
<TextInput type="text" readonly value="{window.location.origin}/note/{result.id}" copy />
<br />
<Button on:click={reset}>new</Button>
{:else}
<form on:submit|preventDefault={submit}>
<fieldset disabled={loading}>
<TextArea label="note" bind:value={note.contents} placeholder="..." />
<div class="bottom">
<Switch label="advanced" bind:value={advanced} />
<Button type="submit">create</Button>
</div>
{#if error}
<div class="error-text">{error}</div>
{/if}
<p><br />{message}</p>
<div class="advanced" class:hidden={!advanced}>
<br />
<div class="fields">
<TextInput
type="number"
label="views"
bind:value={note.views}
disabled={type}
max={100}
/>
<div class="middle-switch">
<Switch label="mode" bind:value={type} color={false} />
</div>
<TextInput
type="number"
label="minutes"
bind:value={note.expiration}
disabled={!type}
max={360}
/>
</div>
<br />
<TextInput
type="password"
label="password"
placeholder="optional"
bind:value={password}
copy
random
/>
</div>
<style>
.fields {
display: flex;
}
.spacer {
width: 3rem;
}
</style>
</fieldset>
</form>
{/if}
<style>
.bottom {
display: flex;
justify-content: space-between;
align-items: flex-end;
margin-top: 0.5rem;
}
.middle-switch {
margin: 0 1rem;
}
.advanced {
max-height: 14em;
overflow: hidden;
transition: var(--ui-anim);
}
.advanced.hidden {
max-height: 0;
}
</style>

BIN
client/src/lib/views/Header/Logo.svg (Stored with Git LFS)

Binary file not shown.

View File

@@ -1,74 +0,0 @@
<script lang="ts">
</script>
<header>
<a href="/">
<svg
width="100%"
height="100%"
viewBox="0 0 475 200"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
xml:space="preserve"
style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;"
fill="currentColor"
><g id="Logo"
><clipPath id="_clip1"><rect x="4.516" y="3.225" width="193.55" height="193.55" /></clipPath
><g clip-path="url(#_clip1)"
><g
><g
><path
d="M171.604,43.296c-2.087,-0 -3.78,1.693 -3.78,3.78c-0,2.087 1.693,3.78 3.78,3.78c2.087,0 3.78,-1.693 3.78,-3.78c0,-2.087 -1.693,-3.78 -3.78,-3.78Z"
style="fill-rule:nonzero;"
/></g
></g
><g
><g
><path
d="M101.291,134.023c-2.087,-0 -3.78,1.693 -3.78,3.78c-0,2.087 1.693,3.78 3.78,3.78c2.087,0 3.78,-1.693 3.78,-3.78c0,-2.087 -1.693,-3.78 -3.78,-3.78Z"
style="fill-rule:nonzero;"
/></g
></g
><g
><g
><path
d="M154.215,96.22c-2.088,-0 -3.78,1.692 -3.78,3.78c-0,18.76 -15.263,34.023 -34.023,34.023c-2.088,-0 -3.78,1.692 -3.78,3.78c-0,2.088 1.692,3.78 3.78,3.78c22.929,0 41.583,-18.654 41.583,-41.583c0,-2.088 -1.692,-3.78 -3.78,-3.78Z"
style="fill-rule:nonzero;"
/></g
></g
><g
><g
><path
d="M197.667,60.507l-9.515,-19.026c-4.101,-8.207 -12.349,-13.306 -21.526,-13.306c-7.48,-0 -14.627,3.547 -19.155,9.498l-10.678,12.016c-2.847,-4.047 -7.021,-7.049 -11.831,-8.421l-19.102,-5.459c-14.623,-4.178 -28.92,-15.441 -39.227,-30.901c-0.924,-1.386 -2.646,-2.003 -4.24,-1.521c-1.595,0.483 -2.685,1.953 -2.685,3.618l-0,20.372c-0,9.468 1.418,18.804 4.219,27.813c-2.936,0.73 -5.896,1.34 -8.842,1.816c-5.773,0.936 -11.654,1.411 -17.48,1.411l-29.309,-0c-1.374,-0 -2.64,0.746 -3.306,1.948c-0.666,1.202 -0.628,2.671 0.101,3.836l22.636,36.219c9.672,15.473 26.33,25.183 44.578,25.983l-36.36,41.158c-5.602,5.728 -3.654,15.315 3.746,18.396l20.018,9.887c0.088,0.044 0.179,0.084 0.271,0.121c5.789,2.313 12.389,-0.496 14.725,-6.279l13.969,-32.982l27.738,0c31.966,0 58.972,-25.967 58.972,-56.704l0,-22.682c0,-6.253 5.088,-11.341 11.341,-11.341l7.561,0c1.31,0 2.527,-0.678 3.216,-1.793c0.688,-1.114 0.751,-2.506 0.165,-3.677Zm-130.399,-42.236c10.418,12.203 23.307,21.034 36.515,24.808l19.104,5.459c3.725,1.063 6.865,3.624 8.666,7.048l-10.049,11.307l-17.652,-10.591c-7.646,-4.588 -16.746,-6.412 -25.776,-4.951c-2.837,0.46 -4.648,1.038 -6.877,1.759c-2.609,-8.332 -3.931,-16.971 -3.931,-25.733l0,-9.106Zm119.457,40.146c-10.422,-0 -18.901,8.479 -18.901,18.901l-0,22.682c-0,26.639 -23.544,49.144 -51.412,49.144l-30.242,-0c-10.771,-0 -20.452,5.983 -25.265,15.615l-0.798,1.596c-0.934,1.867 -0.177,4.137 1.691,5.071c1.867,0.934 4.138,0.176 5.072,-1.691c0.44,-0.586 3.306,-9.102 13.21,-12.125l-12.349,29.159c-0.01,0.024 -0.02,0.047 -0.03,0.071c-0.75,1.877 -2.864,2.851 -4.8,2.148c-21.279,-10.506 -19.997,-9.888 -20.252,-9.99c-2.526,-1.01 -3.191,-4.259 -1.267,-6.182c0.131,-0.131 8.026,-9.078 41.009,-46.411c13.867,-0.617 26.842,-6.319 36.694,-16.172c1.477,-1.476 1.477,-3.87 0,-5.346c-1.476,-1.477 -3.869,-1.476 -5.346,-0c-16.827,16.828 -36.803,13.634 -39.027,14.014c-16.604,0 -31.772,-8.407 -40.574,-22.488l-2.417,-3.868l2.729,1.065c17.308,6.753 38.919,4.347 53.817,-5.586c1.737,-1.158 2.206,-3.505 1.048,-5.242c-1.158,-1.737 -3.505,-2.207 -5.243,-1.048c-13.085,8.724 -31.922,10.666 -46.874,4.832l-12.185,-4.753l-9.896,-15.836l22.488,0c6.23,0 12.518,-0.507 18.688,-1.507c12.711,-2.055 18.051,-4.855 22.993,-5.655c7.181,-1.163 14.516,0.274 20.677,3.97l29.625,17.775c1.79,1.074 4.112,0.494 5.187,-1.296c1.074,-1.79 0.494,-4.113 -1.296,-5.187l-5.377,-3.226c26.559,-29.893 25.139,-28.272 25.319,-28.511c3.102,-4.136 8.038,-6.605 13.205,-6.605c6.293,0 11.95,3.497 14.764,9.127l6.779,13.555l-1.444,-0Z"
style="fill-rule:nonzero;"
/></g
></g
></g
><text
x="195.418px"
y="127.131px"
style="font-family:'Sofia-Regular', 'Sofia';font-size:60.681px;">cryptogeon</text
></g
></svg
>
</a>
</header>
<style>
a {
border: none;
}
header {
text-align: center;
margin-top: calc(min(15vh, 6rem));
margin-bottom: 4rem;
}
header svg {
width: 100%;
max-width: 16rem;
transform: translateX(-1rem);
}
</style>

View File

@@ -1,45 +0,0 @@
<script lang="ts">
import '../app.css'
import Header from '$lib/views/Header/index.svelte'
</script>
<svelte:head>
<title>cryptgeon</title>
</svelte:head>
<main>
<Header />
<slot />
</main>
<footer>
<a href="/">/home</a>
<a href="/about">/about</a>
<a href="https://github.com/cupcakearmy/cryptgeon" target="_blank" rel="noopener">/code</a>
</footer>
<style>
a {
margin: 0 0.5rem;
}
main {
padding: 1rem;
padding-bottom: 4rem;
width: 100%;
max-width: 35rem;
margin: 0 auto;
}
footer {
display: flex;
flex-direction: row;
justify-content: flex-end;
align-items: center;
padding: 1rem;
position: fixed;
bottom: 0;
right: 0;
width: 100%;
background-color: var(--ui-bg-0-85);
}
</style>

View File

@@ -1,66 +0,0 @@
<script context="module">
import { browser, dev } from '$app/env'
export const hydrate = dev
export const router = browser
export const prerender = true
</script>
<svelte:head>
<title>About</title>
</svelte:head>
<section class="content">
<h1>About</h1>
<p>
<i>cryptgeon</i> is an secure, open source sharing note service inspired by
<a href="https://privnote.com"><i>PrivNote</i></a>.
</p>
<p>
<b>▶ how does it work?</b>
<br />
each note has a 512bit generated <i>id</i> that is used to retrieve the note. data is stored in memory
and never persisted to disk.
</p>
<b>▶ Features</b>
<ul>
<li>view and time constrains</li>
<li>in memory, no persistence</li>
<li>in browser encryption → server cannot decrypt contents</li>
</ul>
<p>
<b>▶ tech stack</b>
<br />
the backend is written in rust and the frontend is svelte and typescript.
<br />
you are welcomed to check & audit the
<a href="https://github.com/cupcakearmy/cryptgeon" target="_blank" rel="noopener">source code</a
>.
</p>
<p>
<br />
<b>▶ attributions</b>
<br />
<small>
icons made by <a href="https://www.freepik.com" title="Freepik">freepik</a> from
<a href="https://www.flaticon.com/" title="Flaticon">www.flaticon.com</a>
</small>
</p>
</section>
<style>
section {
width: 100%;
}
ul {
margin: 0;
padding: 0;
padding-left: 1rem;
list-style: square;
}
</style>

View File

@@ -1,98 +0,0 @@
<script context="module" lang="ts">
export async function load({ page }) {
return {
props: page.params,
}
}
</script>
<script lang="ts">
import type { NotePublic } from '$lib/api'
import { info, get } from '$lib/api'
import { decrypt, getKeyFromString } from '$lib/crypto'
import Button from '$lib/ui/Button.svelte'
import TextInput from '$lib/ui/TextInput.svelte'
import copy from 'copy-to-clipboard'
import { onMount } from 'svelte'
export let id: string
let needPassword = false
let password: string = ''
let note: NotePublic | null = null
let exists = false
let loading = true
let error = false
onMount(async () => {
try {
loading = true
error = null
const data = await info(id)
needPassword = data.password
exists = true
} catch {
exists = false
} finally {
loading = false
}
})
async function show() {
const data = note || (await get(id)) // Don't get the content twice on wrong password.
if (needPassword) {
try {
const key = await getKeyFromString(password)
data.contents = await decrypt(data.contents, key)
error = false
} catch {
error = true
}
}
note = data
}
</script>
{#if !loading}
{#if !exists}
<p class="error-text">note was not found or was already deleted.</p>
{:else if note && !error}
<p class="error-text">you will not get the chance to see the note again.</p>
<div class="note">
{note.contents}
</div>
<br />
<Button on:click={() => copy(note.contents)}>copy to clipboard</Button>
{:else}
<form on:submit|preventDefault={show}>
<p>click below to show and delete the note if the counter has reached it's limit</p>
{#if needPassword}
<TextInput type="password" label="password" bind:value={password} />
<br />
{/if}
<Button type="submit">show note</Button>
{#if error}
<br />
<p class="error-text">
wrong password. could not decipher.
<br />
note already destroyed. try again without reloading the page.
</p>
{/if}
</form>
{/if}
{/if}
<style>
.note {
width: 100%;
margin: 0;
padding: 0;
border: 2px solid var(--ui-bg-1);
outline: none;
padding: 0.5rem;
white-space: pre;
overflow: auto;
}
</style>

BIN
client/static/icons/copy-sharp.svg (Stored with Git LFS)

Binary file not shown.

BIN
client/static/icons/dice-sharp.svg (Stored with Git LFS)

Binary file not shown.

BIN
client/static/icons/eye-off-sharp.svg (Stored with Git LFS)

Binary file not shown.

BIN
client/static/icons/eye-sharp.svg (Stored with Git LFS)

Binary file not shown.

BIN
client/static/icons/lock-closed-sharp.svg (Stored with Git LFS)

Binary file not shown.

View File

@@ -1,17 +0,0 @@
const preprocess = require('svelte-preprocess')
const adapter = require('@sveltejs/adapter-static')
/** @type {import('@sveltejs/kit').Config} */
module.exports = {
// Consult https://github.com/sveltejs/svelte-preprocess
// for more information about preprocessors
preprocess: preprocess(),
kit: {
adapter: adapter({
fallback: 'index.html',
}),
// hydrate the <div id="svelte"> element in src/app.html
target: '#svelte',
},
}

View File

@@ -1,30 +0,0 @@
{
"compilerOptions": {
"moduleResolution": "node",
"module": "es2020",
"lib": ["es2020"],
"target": "es2019",
/**
svelte-preprocess cannot figure out whether you have a value or a type, so tell TypeScript
to enforce using \`import type\` instead of \`import\` for Types.
*/
"importsNotUsedAsValues": "error",
"isolatedModules": true,
"resolveJsonModule": true,
/**
To have warnings/errors of the Svelte compiler at the correct position,
enable source maps by default.
*/
"sourceMap": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"baseUrl": ".",
"allowJs": true,
"checkJs": true,
"paths": {
"$lib/*": ["src/lib/*"]
}
},
"include": ["src/**/*.d.ts", "src/**/*.js", "src/**/*.ts", "src/**/*.svelte"]
}

20
cryptgeon.code-workspace Normal file
View File

@@ -0,0 +1,20 @@
{
"folders": [
{
"path": "."
},
{
"path": "packages/backend"
},
{
"path": "packages/frontend"
},
{
"path": "packages/cli"
}
],
"settings": {
"i18n-ally.localesPaths": ["locales"],
"cSpell.words": ["cryptgeon"]
}
}

BIN
design/Github.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

BIN
design/Github_zh-CN.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

BIN
design/Logo.afdesign (Stored with Git LFS)

Binary file not shown.

BIN
design/Logo.svg (Stored with Git LFS)

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 129 B

After

Width:  |  Height:  |  Size: 12 KiB

BIN
design/Screens.afdesign (Stored with Git LFS) Normal file

Binary file not shown.

BIN
design/Screens.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

BIN
design/dove.svg (Stored with Git LFS)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 129 B

After

Width:  |  Height:  |  Size: 3.3 KiB

BIN
design/github.svg (Stored with Git LFS)

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

32
docker-compose.dev.yaml Normal file
View File

@@ -0,0 +1,32 @@
# DEV Compose file.
# For a production file see: README.md
services:
redis:
image: redis:7-alpine
# This is required to stay in RAM only.
command: redis-server --save "" --appendonly no
# Set a size limit. See link below on how to customise.
# https://redis.io/docs/latest/operate/rs/databases/memory-performance/eviction-policy/
# --maxmemory 1gb --maxmemory-policy allkeys-lrulpine
# This prevents the creation of an anonymous volume.
tmpfs:
- /data
ports:
- 6379:6379
app:
build: .
env_file: .env.dev
depends_on:
- redis
restart: unless-stopped
ports:
- 3000:8000
healthcheck:
test: ['CMD', 'curl', '--fail', 'http://127.0.0.1:8000/api/live/']
interval: 1m
timeout: 3s
retries: 2
start_period: 5s

29
docker-compose.yaml Normal file
View File

@@ -0,0 +1,29 @@
services:
redis:
image: redis:7-alpine
# This is required to stay in RAM only.
command: redis-server --save "" --appendonly no
# Set a size limit. See link below on how to customise.
# https://redis.io/docs/latest/operate/rs/databases/memory-performance/eviction-policy/
# --maxmemory 1gb --maxmemory-policy allkeys-lrulpine
# This prevents the creation of an anonymous volume.
tmpfs:
- /data
app:
image: cupcakearmy/cryptgeon:latest
depends_on:
- redis
environment:
# Size limit for a single note.
SIZE_LIMIT: 4 MiB
ports:
- 80:8000
# Optional health checks
# healthcheck:
# test: ["CMD", "curl", "--fail", "http://127.0.0.1:8000/api/live/"]
# interval: 1m
# timeout: 3s
# retries: 2
# start_period: 5s

View File

@@ -1,16 +0,0 @@
# DEV Compose file.
# For a production file see: README.md
version: '3.7'
services:
memcached:
image: memcached:1-alpine
entrypoint: memcached -m 128
ports:
- 11211:11211
app:
build: .
ports:
- 80:5000

View File

@@ -0,0 +1,29 @@
version: '3.8'
services:
redis:
image: redis:7-alpine
# This is required to stay in RAM only.
command: redis-server --save "" --appendonly no
# Set a size limit. See link below on how to customise.
# https://redis.io/docs/latest/operate/rs/databases/memory-performance/eviction-policy/
# --maxmemory 1gb --maxmemory-policy allkeys-lrulpine
# This prevents the creation of an anonymous volume.
tmpfs:
- /data
app:
image: cupcakearmy/cryptgeon:latest
depends_on:
- redis
proxy:
image: nginx:alpine
depends_on:
- app
volumes:
- ./nginx-plain.conf:/etc/nginx/conf.d/default.conf
# Or with tls
# - ./nginx-tls.conf:/etc/nginx/conf.d/default.conf
ports:
- 80:80

View File

@@ -0,0 +1,13 @@
server {
listen 80;
listen [::]:80;
server_name _;
location / {
proxy_pass http://app:8000/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}

View File

@@ -0,0 +1,29 @@
# You should change the server_name to something sensible.
# Also you need to specify the path to the ssl certificates.
server {
listen 80;
listen [::]:80;
server_name _;
# Enforce HTTPS
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name _;
ssl_certificate /path/to/fullchain.pem;
ssl_certificate_key /path/to/privkey.pem;
ssl_trusted_certificate /path/to/fullchain.pem;
location / {
proxy_pass http://app:8000/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}

167
examples/scratch/README.md Normal file
View File

@@ -0,0 +1,167 @@
# Install from scratch.
This is a tiny guide to install cryptgeon on (probably) any unix system (and maybe windows?) from scratch using traefik as the proxy, which will manage certificates and handle https for us.
1. Install Docker & Docker Compose.
2. Install Traefik.
3. Run the cryptgeon.
4. [Optional] install watchtower to keep up to date.
## Install Docker & Docker Compose
- [Docker](https://docs.docker.com/engine/install/)
- [Compose](https://docs.docker.com/compose/install/)
## Install Traefik 2.0
[Traefik](https://doc.traefik.io/traefik/) is a router & proxy that makes deployment of containers incredibly easy. It will manage all the https certificates, routing, etc.
```sh
/foo/bar/traefik/
├── docker-compose.yaml
└── traefik.yaml
```
```yaml
# docker-compose.yaml
version: '3.8'
services:
traefik:
image: traefik:2.6
restart: unless-stopped
ports:
- '80:80'
- '443:443'
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- ./traefik.yaml:/etc/traefik/traefik.yaml:ro
- ./data:/data
labels:
- 'traefik.enable=true'
# HTTP to HTTPS redirection
- 'traefik.http.routers.http_catchall.rule=HostRegexp(`{any:.+}`)'
- 'traefik.http.routers.http_catchall.entrypoints=insecure'
- 'traefik.http.routers.http_catchall.middlewares=https_redirect'
- 'traefik.http.middlewares.https_redirect.redirectscheme.scheme=https'
- 'traefik.http.middlewares.https_redirect.redirectscheme.permanent=true'
networks:
default:
external: true
name: proxy
```
```yaml
# traefik.yaml
api:
dashboard: true
# Define HTTP and HTTPS entrypoint
entryPoints:
insecure:
address: ':80'
secure:
address: ':443'
# Dynamic configuration will come from docker labels
providers:
docker:
endpoint: 'unix:///var/run/docker.sock'
network: 'proxy'
exposedByDefault: false
# Enable acme with http file challenge
certificatesResolvers:
le:
acme:
email: me@example.org
storage: /data/acme.json
httpChallenge:
entryPoint: insecure
```
**Run**
```sh
docker network create proxy
docker-compose up -d
```
## Cryptgeon
Create another docker-compose.yaml file in another folder. We will assume that the domain is `cryptgeon.example.org`.
```sh
/foo/bar/cryptgeon/
└── docker-compose.yaml
```
```yaml
version: '3.8'
networks:
proxy:
external: true
services:
redis:
image: redis:7-alpine
# This is required to stay in RAM only.
command: redis-server --save "" --appendonly no
# Set a size limit. See link below on how to customise.
# https://redis.io/docs/latest/operate/rs/databases/memory-performance/eviction-policy/
# --maxmemory 1gb --maxmemory-policy allkeys-lrulpine
# This prevents the creation of an anonymous volume.
tmpfs:
- /data
app:
image: cupcakearmy/cryptgeon:latest
restart: unless-stopped
depends_on:
- redis
environment:
SIZE_LIMIT: 4 MiB
networks:
- default
- proxy
labels:
- traefik.enable=true
- traefik.http.routers.cryptgeon.rule=Host(`cryptgeon.example.org`)
- traefik.http.routers.cryptgeon.entrypoints=secure
- traefik.http.routers.cryptgeon.tls.certresolver=le
```
**Run**
```sh
docker-compose up -d
```
## Watchtower
> A container-based solution for automating Docker container base image updates.
[Watchtower](https://containrrr.dev/watchtower/) will keep our containers up to date. The interval is set to once a day and also configured to delete old images to prevent cluttering.
```sh
/foo/bar/watchtower/
└── docker-compose.yaml
```
```yaml
# docker-compose.yaml
version: '3.8'
services:
watchtower:
image: containrrr/watchtower
restart: unless-stopped
volumes:
- /var/run/docker.sock:/var/run/docker.sock
command: --cleanup --interval 86400
```

View File

@@ -0,0 +1,91 @@
# Install Cryptgeon with Traefik
Assumptions:
- Traefik 2/3 installed.
- External proxy docker network `proxy`.
- A certificate resolver `le`.
- A https entrypoint `secure`.
- Domain name `example.org`.
```yaml
version: '3.8'
networks:
proxy:
external: true
services:
redis:
image: redis:7-alpine
# This is required to stay in RAM only.
command: redis-server --save "" --appendonly no
# Set a size limit. See link below on how to customise.
# https://redis.io/docs/latest/operate/rs/databases/memory-performance/eviction-policy/
# --maxmemory 1gb --maxmemory-policy allkeys-lrulpine
# This prevents the creation of an anonymous volume.
tmpfs:
- /data
app:
image: cupcakearmy/cryptgeon:latest
restart: unless-stopped
depends_on:
- redis
networks:
- default
- proxy
labels:
- traefik.enable=true
- traefik.http.routers.cryptgeon.rule=Host(`example.org`)
- traefik.http.routers.cryptgeon.entrypoints=secure
- traefik.http.routers.cryptgeon.tls.certresolver=le
```
## With basic auth
Some times it's useful to hide the service behind auth. This is easily achieved with traefik middleware. Many reverse proxies support similar features, so while traefik is used in this example, other reverse proxies can do the same.
```yaml
services:
traefik:
image: traefik:v3.0
command:
- "--api.insecure=true"
- "--providers.docker=true"
- "--providers.docker.exposedbydefault=false"
- "--entrypoints.web.address=:80"
ports:
- "80:80"
volumes:
- "/var/run/docker.sock:/var/run/docker.sock:ro"
redis:
image: redis:7-alpine
# This is required to stay in RAM only.
command: redis-server --save "" --appendonly no
# Set a size limit. See link below on how to customise.
# https://redis.io/docs/latest/operate/rs/databases/memory-performance/eviction-policy/
# --maxmemory 1gb --maxmemory-policy allkeys-lrulpine
# This prevents the creation of an anonymous volume.
tmpfs:
- /data
cryptgeon:
image: cupcakearmy/cryptgeon
depends_on:
- redis
labels:
- "traefik.enable=true"
- "traefik.http.routers.cryptgeon.rule=Host(`cryptgeon.localhost`)"
- "traefik.http.routers.cryptgeon.entrypoints=web"
- "traefik.http.routers.cryptgeon.middlewares=cryptgeon-auth"
- "traefik.http.middlewares.cryptgeon-auth.basicauth.users=user:$$2y$$05$$juUw0zgc5ebvJ00MFPVVLujF6P.rcEMbGZ99Jfq6ZWEa1dgetacEq"
```
```bash
docker compose up -d
```
1. Open http://cryptgeon.localhost
2. Log in with `user` and `secret`

1375
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,11 +1,21 @@
{
"scripts": {
"dev:docker": "docker-compose up memcached",
"dev:backend": "cargo watch -x 'run --bin cryptgeon'",
"dev:front": "npm --prefix client run dev",
"dev": "run-p dev:*"
"dev:docker": "docker compose -f docker-compose.dev.yaml up redis",
"dev:packages": "pnpm --parallel run dev",
"dev": "run-p dev:*",
"docker:up": "docker compose -f docker-compose.dev.yaml up",
"docker:build": "docker compose -f docker-compose.dev.yaml build",
"test": "playwright test --project=chrome --project=firefox --project=safari",
"test:local": "playwright test --project=chrome",
"test:server": "run-s docker:up",
"test:prepare": "run-p build docker:build",
"build": "pnpm run --recursive --filter=!@cryptgeon/backend build"
},
"devDependencies": {
"npm-run-all": "^4.1.5"
}
"@playwright/test": "^1.46.1",
"@types/node": "^22.5.0",
"npm-run-all": "^4.1.5",
"shelljs": "^0.8.5"
},
"packageManager": "pnpm@10.3.0"
}

1613
packages/backend/Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,27 @@
[package]
name = "cryptgeon"
version = "2.9.1"
authors = ["cupcakearmy <hi@nicco.io>"]
edition = "2021"
rust-version = "1.85"
[[bin]]
name = "cryptgeon"
path = "src/main.rs"
[dependencies]
# Core
axum = "0.7.5"
serde = { version = "1.0.208", features = ["derive"] }
tokio = { version = "1.39.3", features = ["full"] }
tower = "0.5.0"
tower-http = { version = "0.5.2", features = ["full"] }
redis = { version = "0.25.2", features = ["tls-native-tls"] }
# Utility
serde_json = "1"
lazy_static = "1"
ring = "0.17"
bs62 = "0.1"
byte-unit = "4"
dotenv = "0.15"

View File

@@ -0,0 +1,10 @@
{
"private": true,
"name": "@cryptgeon/backend",
"scripts": {
"dev": "cargo watch -x 'run --bin cryptgeon'",
"build": "cargo build --release",
"test:server": "SIZE_LIMIT=10MiB LISTEN_ADDR=0.0.0.0:3000 cargo run",
"test:prepare": "cargo build"
}
}

View File

@@ -0,0 +1,73 @@
use byte_unit::Byte;
// Internal
lazy_static! {
pub static ref VERSION: String = option_env!("CARGO_PKG_VERSION")
.unwrap_or("Unknown")
.to_string();
pub static ref FRONTEND_PATH: String =
std::env::var("FRONTEND_PATH").unwrap_or("../frontend/build".to_string());
pub static ref LISTEN_ADDR: String =
std::env::var("LISTEN_ADDR").unwrap_or("0.0.0.0:8000".to_string());
pub static ref VERBOSITY: String = std::env::var("VERBOSITY").unwrap_or("warn".to_string());
}
// CONFIG
lazy_static! {
pub static ref LIMIT: usize =
Byte::from_str(std::env::var("SIZE_LIMIT").unwrap_or("1 KiB".to_string()))
.unwrap()
.get_bytes() as usize;
pub static ref MAX_VIEWS: u32 = std::env::var("MAX_VIEWS")
.unwrap_or("100".to_string())
.parse()
.unwrap();
pub static ref MAX_EXPIRATION: u32 = std::env::var("MAX_EXPIRATION")
.unwrap_or("360".to_string()) // 6 hours in minutes
.parse()
.unwrap();
pub static ref ALLOW_ADVANCED: bool = std::env::var("ALLOW_ADVANCED")
.unwrap_or("true".to_string())
.parse()
.unwrap();
pub static ref ID_LENGTH: u32 = std::env::var("ID_LENGTH")
.unwrap_or("32".to_string())
.parse()
.unwrap();
pub static ref ALLOW_FILES: bool = std::env::var("ALLOW_FILES")
.unwrap_or("true".to_string())
.parse()
.unwrap();
pub static ref IMPRINT_URL: String = std::env::var("IMPRINT_URL")
.unwrap_or("".to_string())
.parse()
.unwrap();
pub static ref IMPRINT_HTML: String = std::env::var("IMPRINT_HTML")
.unwrap_or("".to_string())
.parse()
.unwrap();
}
// THEME
lazy_static! {
pub static ref THEME_IMAGE: String = std::env::var("THEME_IMAGE")
.unwrap_or("".to_string())
.parse()
.unwrap();
pub static ref THEME_TEXT: String = std::env::var("THEME_TEXT")
.unwrap_or("".to_string())
.parse()
.unwrap();
pub static ref THEME_PAGE_TITLE: String = std::env::var("THEME_PAGE_TITLE")
.unwrap_or("".to_string())
.parse()
.unwrap();
pub static ref THEME_FAVICON: String = std::env::var("THEME_FAVICON")
.unwrap_or("".to_string())
.parse()
.unwrap();
pub static ref THEME_NEW_NOTE_NOTICE: bool = std::env::var("THEME_NEW_NOTE_NOTICE")
.unwrap_or("true".to_string())
.parse()
.unwrap();
}

View File

@@ -0,0 +1,16 @@
use axum::{body::Body, extract::Request, http::HeaderValue, middleware::Next, response::Response};
const CUSTOM_HEADER_NAME: &str = "Content-Security-Policy";
const CUSTOM_HEADER_VALUE: &str = "default-src 'self'; script-src 'report-sample' 'self'; style-src 'report-sample' 'self'; object-src 'none'; base-uri 'self'; connect-src 'self' data:; font-src 'self'; frame-src 'self'; img-src 'self'; manifest-src 'self'; media-src 'self'; worker-src 'none';";
lazy_static! {
static ref HEADER_VALUE: HeaderValue = HeaderValue::from_static(CUSTOM_HEADER_VALUE);
}
pub async fn add_csp_header(request: Request<Body>, next: Next) -> Response {
let mut response = next.run(request).await;
response
.headers_mut()
.append(CUSTOM_HEADER_NAME, HEADER_VALUE.clone());
response
}

View File

@@ -0,0 +1,10 @@
use crate::store;
use axum::http::StatusCode;
pub async fn report_health() -> (StatusCode,) {
if store::can_reach_redis() {
return (StatusCode::OK,);
} else {
return (StatusCode::SERVICE_UNAVAILABLE,);
}
}

View File

@@ -0,0 +1,10 @@
use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::Mutex;
#[derive(Clone)]
pub struct SharedState {
pub locks: LockMap,
}
pub type LockMap = Arc<Mutex<HashMap<String, Arc<Mutex<()>>>>>;

View File

@@ -0,0 +1,80 @@
use std::{collections::HashMap, sync::Arc};
use axum::{
extract::{DefaultBodyLimit, Request},
routing::{delete, get, post},
Router, ServiceExt,
};
use dotenv::dotenv;
use lock::SharedState;
use tokio::sync::Mutex;
use tower::Layer;
use tower_http::{
compression::CompressionLayer,
normalize_path::NormalizePathLayer,
services::{ServeDir, ServeFile},
};
#[macro_use]
extern crate lazy_static;
mod config;
mod csp;
mod health;
mod lock;
mod note;
mod status;
mod store;
#[tokio::main]
async fn main() {
dotenv().ok();
let shared_state = SharedState {
locks: Arc::new(Mutex::new(HashMap::new())),
};
if !store::can_reach_redis() {
println!("cannot reach redis");
panic!("cannot reach redis");
}
let notes_routes = Router::new()
.route("/", post(note::create))
.route("/:id", delete(note::delete))
.route("/:id", get(note::preview));
let health_routes = Router::new().route("/live", get(health::report_health));
let status_routes = Router::new().route("/status", get(status::get_status));
let api_routes = Router::new()
.nest("/notes", notes_routes)
.nest("/", health_routes)
.nest("/", status_routes);
let index = format!("{}{}", config::FRONTEND_PATH.to_string(), "/index.html");
let serve_dir =
ServeDir::new(config::FRONTEND_PATH.to_string()).not_found_service(ServeFile::new(index));
let app = Router::new()
.nest("/api", api_routes)
.fallback_service(serve_dir)
// Disabled for now, as svelte inlines scripts
// .layer(middleware::from_fn(csp::add_csp_header))
.layer(DefaultBodyLimit::max(*config::LIMIT))
.layer(
CompressionLayer::new()
.br(true)
.deflate(true)
.gzip(true)
.zstd(true),
)
.with_state(shared_state);
let app = NormalizePathLayer::trim_trailing_slash().layer(app);
let listener = tokio::net::TcpListener::bind(config::LISTEN_ADDR.to_string())
.await
.unwrap();
println!("listening on {}", listener.local_addr().unwrap());
axum::serve(listener, ServiceExt::<Request>::into_make_service(app))
.await
.unwrap();
}

View File

@@ -0,0 +1,36 @@
use bs62;
use ring::rand::SecureRandom;
use serde::{Deserialize, Serialize};
use crate::config;
#[derive(Serialize, Deserialize, Clone)]
pub struct Note {
pub meta: String,
pub contents: String,
pub views: Option<u32>,
pub expiration: Option<u32>,
}
#[derive(Serialize)]
pub struct NoteInfo {
pub meta: String,
}
#[derive(Serialize)]
pub struct NotePublic {
pub meta: String,
pub contents: String,
}
pub fn generate_id() -> String {
let mut result = "".to_owned();
let mut id: [u8; 1] = [0; 1];
let sr = ring::rand::SystemRandom::new();
for _ in 0..*config::ID_LENGTH {
let _ = sr.fill(&mut id);
result.push_str(&bs62::encode_data(&id));
}
return result;
}

View File

@@ -0,0 +1,156 @@
use axum::{
extract::Path,
http::StatusCode,
response::{IntoResponse, Response},
Json,
};
use serde::{Deserialize, Serialize};
use std::{sync::Arc, time::SystemTime};
use tokio::sync::Mutex;
use crate::note::{generate_id, Note, NoteInfo};
use crate::store;
use crate::{config, lock::SharedState};
use super::NotePublic;
pub fn now() -> u32 {
SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap()
.as_secs() as u32
}
#[derive(Deserialize)]
pub struct OneNoteParams {
id: String,
}
pub async fn preview(Path(OneNoteParams { id }): Path<OneNoteParams>) -> Response {
let note = store::get(&id);
match note {
Ok(Some(n)) => (StatusCode::OK, Json(NoteInfo { meta: n.meta })).into_response(),
Ok(None) => (StatusCode::NOT_FOUND).into_response(),
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
}
}
#[derive(Serialize, Deserialize)]
struct CreateResponse {
id: String,
}
pub async fn create(Json(mut n): Json<Note>) -> Response {
// let mut n = note.into_inner();
let id = generate_id();
// let bad_req = HttpResponse::BadRequest().finish();
if n.views == None && n.expiration == None {
return (
StatusCode::BAD_REQUEST,
"At least views or expiration must be set",
)
.into_response();
}
if !*config::ALLOW_ADVANCED {
n.views = Some(1);
n.expiration = None;
}
match n.views {
Some(v) => {
if v > *config::MAX_VIEWS || v < 1 {
return (StatusCode::BAD_REQUEST, "Invalid views").into_response();
}
n.expiration = None; // views overrides expiration
}
_ => {}
}
match n.expiration {
Some(e) => {
if e > *config::MAX_EXPIRATION || e < 1 {
return (StatusCode::BAD_REQUEST, "Invalid expiration").into_response();
}
let expiration = now() + (e * 60);
n.expiration = Some(expiration);
}
_ => {}
}
match store::set(&id.clone(), &n.clone()) {
Ok(_) => (StatusCode::OK, Json(CreateResponse { id })).into_response(),
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
}
}
pub async fn delete(
Path(OneNoteParams { id }): Path<OneNoteParams>,
state: axum::extract::State<SharedState>,
) -> Response {
let mut locks_map = state.locks.lock().await;
let lock = locks_map
.entry(id.clone())
.or_insert_with(|| Arc::new(Mutex::new(())))
.clone();
drop(locks_map);
let _guard = lock.lock().await;
let note = store::get(&id);
match note {
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
Ok(None) => (StatusCode::NOT_FOUND).into_response(),
Ok(Some(note)) => {
let mut changed = note.clone();
if changed.views == None && changed.expiration == None {
return (StatusCode::BAD_REQUEST).into_response();
}
match changed.views {
Some(v) => {
changed.views = Some(v - 1);
let id = id.clone();
if v <= 1 {
match store::del(&id) {
Err(e) => {
return (StatusCode::INTERNAL_SERVER_ERROR, e.to_string())
.into_response();
}
_ => {}
}
} else {
match store::set(&id, &changed.clone()) {
Err(e) => {
return (StatusCode::INTERNAL_SERVER_ERROR, e.to_string())
.into_response();
}
_ => {}
}
}
}
_ => {}
}
let n = now();
match changed.expiration {
Some(e) => {
if e < n {
match store::del(&id.clone()) {
Ok(_) => return (StatusCode::BAD_REQUEST).into_response(),
Err(e) => {
return (StatusCode::INTERNAL_SERVER_ERROR, e.to_string())
.into_response()
}
}
}
}
_ => {}
}
return (
StatusCode::OK,
Json(NotePublic {
contents: changed.contents,
meta: changed.meta,
}),
)
.into_response();
}
}
}

View File

@@ -0,0 +1,43 @@
use crate::config;
use axum::{http::StatusCode, Json};
use serde::Serialize;
#[derive(Serialize)]
pub struct Status {
// General
pub version: String,
// Config
pub max_size: u32,
pub max_views: u32,
pub max_expiration: u32,
pub allow_advanced: bool,
pub allow_files: bool,
pub imprint_url: String,
pub imprint_html: String,
// Theme
pub theme_image: String,
pub theme_text: String,
pub theme_page_title: String,
pub theme_favicon: String,
pub theme_new_note_notice: bool,
}
pub async fn get_status() -> (StatusCode, Json<Status>) {
let status = Status {
version: config::VERSION.to_string(),
max_size: *config::LIMIT as u32,
max_views: *config::MAX_VIEWS,
max_expiration: *config::MAX_EXPIRATION,
allow_advanced: *config::ALLOW_ADVANCED,
allow_files: *config::ALLOW_FILES,
imprint_url: config::IMPRINT_URL.to_string(),
imprint_html: config::IMPRINT_HTML.to_string(),
theme_new_note_notice: *config::THEME_NEW_NOTE_NOTICE,
theme_image: config::THEME_IMAGE.to_string(),
theme_text: config::THEME_TEXT.to_string(),
theme_page_title: config::THEME_PAGE_TITLE.to_string(),
theme_favicon: config::THEME_FAVICON.to_string(),
};
(StatusCode::OK, Json(status))
}

View File

@@ -0,0 +1,63 @@
use redis;
use redis::Commands;
use crate::note::now;
use crate::note::Note;
lazy_static! {
static ref REDIS_CLIENT: String = std::env::var("REDIS")
.unwrap_or("redis://127.0.0.1/".to_string())
.parse()
.unwrap();
}
fn get_connection() -> Result<redis::Connection, &'static str> {
let client =
redis::Client::open(REDIS_CLIENT.to_string()).map_err(|_| "Unable to connect to redis")?;
client
.get_connection()
.map_err(|_| "Unable to connect to redis")
}
pub fn can_reach_redis() -> bool {
let conn = get_connection();
return match conn {
Ok(_) => true,
Err(_) => false,
};
}
pub fn set(id: &String, note: &Note) -> Result<(), &'static str> {
let serialized = serde_json::to_string(&note.clone()).unwrap();
let mut conn = get_connection()?;
conn.set(id, serialized)
.map_err(|_| "Unable to set note in redis")?;
match note.expiration {
Some(e) => {
let seconds = e - now();
conn.expire(id, seconds as i64)
.map_err(|_| "Unable to set expiration on notion")?
}
None => {}
};
Ok(())
}
pub fn get(id: &String) -> Result<Option<Note>, &'static str> {
let mut conn = get_connection()?;
let value: Option<String> = conn.get(id).map_err(|_| "Could not load note in redis")?;
match value {
None => return Ok(None),
Some(s) => {
let deserialize: Note = serde_json::from_str(&s).unwrap();
return Ok(Some(deserialize));
}
}
}
pub fn del(id: &String) -> Result<(), &'static str> {
let mut conn = get_connection()?;
conn.del(id).map_err(|_| "Unable to delete note in redis")?;
Ok(())
}

54
packages/cli/README.md Normal file
View File

@@ -0,0 +1,54 @@
# Cryptgeon CLI
The CLI is a functionally identical way to interact with cryptgeon notes.
It supports text, files, expiration, password, etc.
## Installation
```bash
npx cryptgeon
# Or install globally
npm -g install cryptgeon
cryptgeon
```
## Examples
```bash
# Create simple note
cryptgeon send text "Foo bar"
# Send two files
cryptgeon send file my.pdf picture.png
# 3 views
cryptgeon send text "My message" --views 3
# 10 minutes
cryptgeon send text "My message" --minutes 10
# Custom password
cryptgeon send text "My message" --password "1337"
# Password from stdin
echo "1337" | cryptgeon send text "My message"
# Open a link
cryptgeon open https://cryptgeon.org/note/16gOIkxWjCxYNuXM8tCqMUzl...
```
## Options
### Custom server
The default server is `cryptgeon.org`, however you can use any cryptgeon server by passing the `-s` or `--server` option, or by setting the `CRYPTGEON_SERVER` environment variable.
### Password
Optionally, just like in the web ui, you can choose to use a manual password. You can do that by passing the `-p` or `--password` options, or by piping it into stdin.
```bash
echo "my pw" | cryptgeon send text "my text"
cat pass.txt | cryptgeon send text "my text"
```

15
packages/cli/build.js Normal file
View File

@@ -0,0 +1,15 @@
import { build } from 'tsup'
import pkg from './package.json' with { type: 'json' }
const watch = process.argv.slice(2)[0] === '--watch'
await build({
entry: ['src/index.ts', 'src/cli.ts', 'src/shared/shared.ts'],
dts: true,
minify: true,
format: ['esm', 'cjs'],
target: 'es2020',
clean: true,
define: { VERSION: `"${pkg.version}"` },
watch,
})

47
packages/cli/package.json Normal file
View File

@@ -0,0 +1,47 @@
{
"name": "cryptgeon",
"version": "2.9.1",
"homepage": "https://github.com/cupcakearmy/cryptgeon",
"repository": {
"type": "git",
"url": "https://github.com/cupcakearmy/cryptgeon.git",
"directory": "packages/cli"
},
"type": "module",
"exports": {
".": "./dist/index.js",
"./shared": {
"import": "./dist/shared/shared.js",
"types": "./dist/shared/shared.d.ts"
}
},
"types": "./dist/index.d.ts",
"bin": {
"cryptgeon": "./dist/cli.cjs"
},
"files": [
"dist"
],
"scripts": {
"bin": "run-s build package",
"build": "tsc && node build.js",
"dev": "node build.js --watch",
"prepublishOnly": "run-s build"
},
"devDependencies": {
"@commander-js/extra-typings": "^12.1.0",
"@types/inquirer": "^9.0.7",
"@types/mime": "^4.0.0",
"@types/node": "^20.11.24",
"commander": "^12.1.0",
"inquirer": "^9.2.15",
"mime": "^4.0.1",
"occulto": "^2.0.6",
"pretty-bytes": "^6.1.1",
"tsup": "^8.2.4",
"typescript": "^5.3.3"
},
"engines": {
"node": ">=18"
}
}

View File

@@ -0,0 +1,90 @@
import inquirer from 'inquirer'
import { access, constants, writeFile } from 'node:fs/promises'
import { basename, resolve } from 'node:path'
import { AES, Hex } from 'occulto'
import pretty from 'pretty-bytes'
import { Adapters } from '../shared/adapters.js'
import { API } from '../shared/api.js'
export async function download(url: URL, all: boolean, suggestedPassword?: string) {
API.setOptions({ server: url.origin })
const id = url.pathname.split('/')[2]
const preview = await API.info(id).catch(() => {
throw new Error('Note does not exist or is expired')
})
// Password
let password: string
const derivation = preview?.meta.derivation
if (derivation) {
if (suggestedPassword) {
password = suggestedPassword
} else {
const response = await inquirer.prompt([
{
type: 'password',
message: 'Note password',
name: 'password',
},
])
password = response.password
}
} else {
password = url.hash.slice(1)
}
const key = derivation ? (await AES.derive(password, derivation))[0] : Hex.decode(password)
const note = await API.get(id)
const couldNotDecrypt = new Error('Could not decrypt note. Probably an invalid password')
switch (note.meta.type) {
case 'file':
const files = await Adapters.Files.decrypt(note.contents, key).catch(() => {
throw couldNotDecrypt
})
if (!files) {
throw new Error('No files found in note')
}
let selected: typeof files
if (all) {
selected = files
} else {
const { names } = await inquirer.prompt([
{
type: 'checkbox',
message: 'What files should be saved?',
name: 'names',
choices: files.map((file) => ({
value: file.name,
name: `${file.name} - ${file.type} - ${pretty(file.size, { binary: true })}`,
checked: true,
})),
},
])
selected = files.filter((file) => names.includes(file.name))
}
if (!selected.length) throw new Error('No files selected')
await Promise.all(
selected.map(async (file) => {
let filename = resolve(file.name)
try {
// If exists -> prepend timestamp to not overwrite the current file
await access(filename, constants.R_OK)
filename = resolve(`${Date.now()}-${file.name}`)
} catch {}
await writeFile(filename, file.contents)
console.log(`Saved: ${basename(filename)}`)
})
)
break
case 'text':
const plaintext = await Adapters.Text.decrypt(note.contents, key).catch(() => {
throw couldNotDecrypt
})
console.log(plaintext)
break
}
}

View File

@@ -0,0 +1,46 @@
import { readFile, stat } from 'node:fs/promises'
import { basename } from 'node:path'
import mime from 'mime'
import { AES, Hex } from 'occulto'
import { Adapters } from '../shared/adapters.js'
import { API, FileDTO, Note, NoteMeta } from '../shared/api.js'
export type UploadOptions = Pick<Note, 'views' | 'expiration'> & { password?: string }
export async function upload(input: string | string[], options: UploadOptions): Promise<string> {
const { password, ...noteOptions } = options
const derived = options.password ? await AES.derive(options.password) : undefined
const key = derived ? derived[0] : await AES.generateKey()
let contents: string
let type: NoteMeta['type']
if (typeof input === 'string') {
contents = await Adapters.Text.encrypt(input, key)
type = 'text'
} else {
const files: FileDTO[] = await Promise.all(
input.map(async (path) => {
const data = new Uint8Array(await readFile(path))
const stats = await stat(path)
const extension = path.substring(path.indexOf('.') + 1)
const type = mime.getType(extension) ?? 'application/octet-stream'
return {
name: basename(path),
size: stats.size,
contents: data,
type,
} satisfies FileDTO
})
)
contents = await Adapters.Files.encrypt(files, key)
type = 'file'
}
// Create the actual note and upload it.
const note: Note = { ...noteOptions, contents, meta: { type, derivation: derived?.[1] } }
const result = await API.create(note)
let url = `${API.getOptions().server}/note/${result.id}`
if (!derived) url += `#${Hex.encode(key)}`
return url
}

106
packages/cli/src/cli.ts Normal file
View File

@@ -0,0 +1,106 @@
#!/usr/bin/env node
import { Argument, Option, program } from '@commander-js/extra-typings'
import prettyBytes from 'pretty-bytes'
import { download } from './actions/download.js'
import { upload } from './actions/upload.js'
import { API } from './shared/api.js'
import { parseFile, parseNumber } from './utils/parsers.js'
import { getStdin } from './utils/stdin.js'
import { checkConstrains, exit } from './utils/utils.js'
const defaultServer = process.env['CRYPTGEON_SERVER'] || 'https://cryptgeon.org'
const server = new Option('-s --server <url>', 'the cryptgeon server to use').default(defaultServer)
const files = new Argument('<file...>', 'Files to be sent').argParser(parseFile)
const text = new Argument('<text>', 'Text content of the note')
const password = new Option('-p --password <string>', 'manually set a password')
const all = new Option('-a --all', 'Save all files without prompt').default(false)
const url = new Argument('<url>', 'The url to open')
const views = new Option('-v --views <number>', 'Amount of views before getting destroyed').argParser(parseNumber)
const minutes = new Option('-m --minutes <number>', 'Minutes before the note expires').argParser(parseNumber)
// Node 18 guard
parseInt(process.version.slice(1).split(',')[0]) < 18 && exit('Node 18 or higher is required')
// @ts-ignore
const version: string = VERSION
program.name('cryptgeon').version(version).configureHelp({ showGlobalOptions: true })
program
.command('info')
.description('show information about the server')
.addOption(server)
.action(async (options) => {
API.setOptions({ server: options.server })
const response = await API.status()
const formatted = {
...response,
max_size: prettyBytes(response.max_size),
}
for (const key of Object.keys(formatted)) {
if (key.startsWith('theme_')) delete formatted[key as keyof typeof formatted]
}
console.table(formatted)
})
const send = program.command('send').description('send a note')
send
.command('file')
.addArgument(files)
.addOption(server)
.addOption(views)
.addOption(minutes)
.addOption(password)
.action(async (files, options) => {
API.setOptions({ server: options.server })
await checkConstrains(options)
options.password ||= await getStdin()
try {
const url = await upload(files, { views: options.views, expiration: options.minutes, password: options.password })
console.log(`Note created:\n\n${url}`)
} catch {
exit('Could not create note')
}
})
send
.command('text')
.addArgument(text)
.addOption(server)
.addOption(views)
.addOption(minutes)
.addOption(password)
.action(async (text, options) => {
API.setOptions({ server: options.server })
await checkConstrains(options)
options.password ||= await getStdin()
try {
const url = await upload(text, { views: options.views, expiration: options.minutes, password: options.password })
console.log(`Note created:\n\n${url}`)
} catch {
exit('Could not create note')
}
})
program
.command('open')
.description('open a link with text or files inside')
.addArgument(url)
.addOption(password)
.addOption(all)
.action(async (note, options) => {
try {
const url = new URL(note)
options.password ||= await getStdin()
try {
await download(url, options.all, options.password)
} catch (e) {
exit(e instanceof Error ? e.message : 'Unknown error occurred')
}
} catch {
exit('Invalid URL')
}
})
program.parse()

View File

@@ -0,0 +1,4 @@
export * from './actions/download.js'
export * from './actions/upload.js'
export * from './shared/adapters.js'
export * from './shared/api.js'

View File

@@ -0,0 +1,61 @@
import { AES, Bytes, type TypedArray } from 'occulto'
import type { EncryptedFileDTO, FileDTO } from './api'
abstract class CryptAdapter<T> {
abstract encrypt(plaintext: T, key: TypedArray): Promise<string>
abstract decrypt(ciphertext: string, key: TypedArray): Promise<T>
}
class CryptTextAdapter implements CryptAdapter<string> {
async encrypt(plaintext: string, key: TypedArray) {
return await AES.encrypt(Bytes.encode(plaintext), key)
}
async decrypt(ciphertext: string, key: TypedArray) {
return Bytes.decode(await AES.decrypt(ciphertext, key))
}
}
class CryptBlobAdapter implements CryptAdapter<TypedArray> {
async encrypt(plaintext: TypedArray, key: TypedArray) {
return await AES.encrypt(plaintext, key)
}
async decrypt(ciphertext: string, key: TypedArray) {
return await AES.decrypt(ciphertext, key)
// const plaintext = await AES.decrypt(ciphertext, key)
// return new Blob([plaintext], { type: 'application/octet-stream' })
}
}
class CryptFilesAdapter implements CryptAdapter<FileDTO[]> {
async encrypt(plaintext: FileDTO[], key: TypedArray) {
const adapter = new CryptBlobAdapter()
const data: Promise<EncryptedFileDTO>[] = plaintext.map(async (file) => ({
name: file.name,
size: file.size,
type: file.type,
contents: await adapter.encrypt(file.contents, key),
}))
return JSON.stringify(await Promise.all(data))
}
async decrypt(ciphertext: string, key: TypedArray) {
const adapter = new CryptBlobAdapter()
const data: EncryptedFileDTO[] = JSON.parse(ciphertext)
const files: FileDTO[] = await Promise.all(
data.map(async (file) => ({
name: file.name,
size: file.size,
type: file.type,
contents: await adapter.decrypt(file.contents, key),
}))
)
return files
}
}
export const Adapters = {
Text: new CryptTextAdapter(),
Blob: new CryptBlobAdapter(),
Files: new CryptFilesAdapter(),
}

View File

@@ -0,0 +1,140 @@
import type { KeyData, TypedArray } from 'occulto'
export type NoteMeta = {
type: 'text' | 'file'
derivation?: KeyData
}
export type Note = {
contents: string
meta: NoteMeta
views?: number
expiration?: number
}
export type NoteInfo = Pick<Note, 'meta'>
export type NotePublic = Pick<Note, 'contents' | 'meta'>
export type NoteCreate = Omit<Note, 'meta'> & { meta: string }
export type FileDTO = Pick<File, 'name' | 'size' | 'type'> & {
contents: TypedArray
}
export type EncryptedFileDTO = Omit<FileDTO, 'contents'> & {
contents: string
}
type ClientOptions = {
server: string
}
type CallOptions = {
url: string
method: string
body?: any
}
export class PayloadToLargeError extends Error {}
export let client: ClientOptions = {
server: '',
}
function setOptions(options: Partial<ClientOptions>) {
client = { ...client, ...options }
}
function getOptions(): ClientOptions {
return client
}
async function call(options: CallOptions) {
const url = client.server + '/api/' + options.url
const response = await fetch(url, {
method: options.method,
body: options.body === undefined ? undefined : JSON.stringify(options.body),
mode: 'cors',
headers: {
'Content-Type': 'application/json',
},
})
if (!response.ok) {
if (response.status === 413) throw new PayloadToLargeError()
else throw new Error('API call failed')
}
return response.json()
}
async function create(note: Note) {
const { meta, ...rest } = note
const body: NoteCreate = {
...rest,
meta: JSON.stringify(meta),
}
const data = await call({
url: 'notes/',
method: 'post',
body,
})
return data as { id: string }
}
async function get(id: string): Promise<NotePublic> {
const data = await call({
url: `notes/${id}`,
method: 'delete',
})
const { contents, meta } = data
const note = {
contents,
meta: JSON.parse(meta),
} satisfies NotePublic
if (note.meta.derivation) note.meta.derivation.salt = new Uint8Array(Object.values(note.meta.derivation.salt))
return note
}
async function info(id: string): Promise<NoteInfo> {
const data = await call({
url: `notes/${id}`,
method: 'get',
})
const { meta } = data
const note = {
meta: JSON.parse(meta),
} satisfies NoteInfo
if (note.meta.derivation) note.meta.derivation.salt = new Uint8Array(Object.values(note.meta.derivation.salt))
return note
}
export type Status = {
version: string
max_size: number
max_views: number
max_expiration: number
allow_advanced: boolean
allow_files: boolean
imprint_url: string
imprint_html: string
theme_image: string
theme_text: string
theme_favicon: string
theme_page_title: string
theme_new_note_notice: boolean
}
async function status() {
const data = await call({
url: 'status/',
method: 'get',
})
return data as Status
}
export const API = {
setOptions,
getOptions,
create,
get,
info,
status,
}

View File

@@ -0,0 +1,2 @@
export * from './adapters.js'
export * from './api.js'

View File

@@ -0,0 +1,27 @@
import { InvalidArgumentError, InvalidOptionArgumentError } from '@commander-js/extra-typings'
import { accessSync, constants } from 'node:fs'
import { resolve } from 'node:path'
export function parseFile(value: string, before: string[] = []) {
try {
const file = resolve(value)
accessSync(file, constants.R_OK)
return [...before, file]
} catch {
throw new InvalidArgumentError('cannot access file')
}
}
export function parseURL(value: string, _: URL): URL {
try {
return new URL(value)
} catch {
throw new InvalidArgumentError('is not a valid url')
}
}
export function parseNumber(value: string, _: number): number {
const n = Number.parseInt(value, 10)
if (Number.isNaN(n)) throw new InvalidOptionArgumentError('invalid number')
return n
}

View File

@@ -0,0 +1,25 @@
export function getStdin(timeout: number = 10): Promise<string> {
return new Promise<string>((resolve, reject) => {
// Store the data from stdin in a buffer
let buffer = ''
let t: NodeJS.Timeout
const dataHandler = (d: Buffer) => (buffer += d.toString())
const endHandler = () => {
clearTimeout(t)
resolve(buffer.trim())
}
// Stop listening for data after the timeout, otherwise hangs indefinitely
t = setTimeout(() => {
process.stdin.removeListener('data', dataHandler)
process.stdin.removeListener('end', endHandler)
process.stdin.pause()
resolve('')
}, timeout)
process.stdin.on('error', reject)
process.stdin.on('data', dataHandler)
process.stdin.on('end', endHandler)
})
}

View File

@@ -0,0 +1,19 @@
import { exit as exitNode } from 'node:process'
import { API } from '../shared/api.js'
export function exit(message: string) {
console.error(message)
exitNode(1)
}
export async function checkConstrains(constrains: { views?: number; minutes?: number }) {
const { views, minutes } = constrains
if (views && minutes) exit('cannot set view and minutes constrains simultaneously')
if (!views && !minutes) constrains.views = 1
const response = await API.status()
if (views && views > response.max_views)
exit(`Only a maximum of ${response.max_views} views allowed. ${views} given.`)
if (minutes && minutes > response.max_expiration)
exit(`Only a maximum of ${response.max_expiration} minutes allowed. ${minutes} given.`)
}

View File

@@ -0,0 +1,13 @@
{
"compilerOptions": {
"target": "es2022",
"module": "es2022",
"moduleResolution": "Bundler",
"declaration": true,
"emitDeclarationOnly": true,
"strict": true,
"outDir": "./dist",
"rootDir": "./src",
"allowSyntheticDefaultImports": true
}
}

View File

@@ -1,5 +1,6 @@
.DS_Store
node_modules
/.svelte
/.svelte-kit
/build
/functions

View File

@@ -0,0 +1,18 @@
# Cryptgeon Frontend
## Locale
Download with these settings:
```json
{
"format": "json",
"indentation": "tab",
"json_unescaped_slashes": true,
"export_sort": "first_added",
"original_filenames": false,
"export_empty_as": "skip",
"add_newline_eof": true,
"replace_breaks": false
}
```

View File

@@ -0,0 +1,8 @@
├─ MIT: 13
├─ ISC: 2
├─ BSD-3-Clause: 1
├─ (MPL-2.0 OR Apache-2.0): 1
├─ BSD-2-Clause: 1
├─ 0BSD: 1
└─ Apache-2.0: 1
1 ├─ MIT: 13
2 ├─ ISC: 2
3 ├─ BSD-3-Clause: 1
4 ├─ (MPL-2.0 OR Apache-2.0): 1
5 ├─ BSD-2-Clause: 1
6 ├─ 0BSD: 1
7 └─ Apache-2.0: 1

View File

@@ -0,0 +1,58 @@
{
"common": {
"note": "Notiz",
"file": "Datei",
"advanced": "Erweiterte Optionen",
"create": "Erstellen",
"loading": "Lädt...",
"mode": "Modus",
"views": "{n, plural, =0 {Ansichten} =1 {1 Ansicht} other {# Ansichten}}",
"minutes": "{n, plural, =0 {Minuten} =1 {1 Minute} other {# Minuten}}",
"max": "max",
"share_link": "Link teilen",
"copy_clipboard": "In die Zwischenablage kopieren",
"copied_to_clipboard": "In die Zwischenablage kopiert.",
"encrypting": "Wird verschlüsselt...",
"decrypting": "Wird entschlüsselt...",
"uploading": "Hochladen",
"downloading": "Wird heruntergeladen",
"qr_code": "QR-Code",
"password": "Passwort"
},
"home": {
"intro": "Erstellen Sie mit einem Klick <i>vollständig verschlüsselte</i>, sichere Notizen oder Dateien und teilen Sie diese über einen Link.",
"explanation": "Die Notiz verfällt nach {type}.",
"new_note": "Neue Notiz",
"new_note_notice": "<b>Wichtiger Hinweis zur Verfügbarkeit:</b><br />Es kann nicht garantiert werden, dass diese Notiz gespeichert wird, da diese <b>ausschließlich im Speicher</b> gehalten werden. Ist dieser voll, werden die ältesten Notizen entfernt.<br />(Wahrscheinlich gibt es keine derartigen Probleme, seien Sie nur vorgewarnt).",
"errors": {
"note_to_big": "Notiz konnte nicht erstellt werden, da sie zu groß ist.",
"note_error": "Notiz konnte nicht erstellt werden. Bitte versuchen Sie es erneut.",
"max": "max: {n}",
"empty_content": "Notiz ist leer."
},
"messages": {
"note_created": "Notiz wurde erstellt."
},
"advanced": {
"explanation": "Standardmäßig wird für jede Notiz ein generiertes, sicheres Passwort verwendet. Alternativ können Sie ein eigenes Kennwort festlegen, welches nicht im Link enthalten ist.",
"custom_password": "Benutzerdefiniertes Passwort"
}
},
"show": {
"errors": {
"not_found": "Notiz konnte nicht gefunden werden oder wurde bereits gelöscht.",
"decryption_failed": "Notiz konnte nicht entschlüsselt werden. Vermutlich ist das Passwort falsch oder der Link defekt. Die Notiz wurde daher gelöscht.",
"unsupported_type": "Nicht unterstützter Notiztyp."
},
"explanation": "Klicken Sie auf den Button, um die Notiz anzuzeigen und anschließend zu löschen, falls ein festgelegtes Limit erreicht wurde.",
"show_note": "Notiz anzeigen",
"warning_will_not_see_again": "ACHTUNG! Sie werden anschließend <b>keine</b> Gelegenheit mehr haben, die Notiz erneut anzusehen.",
"download_all": "Alle Dateien herunterladen",
"links_found": "Gefundene Links in der Notiz:"
},
"file_upload": {
"selected_files": "Ausgewählte Dateien",
"no_files_selected": "Keine Dateien ausgewählt",
"clear": "Zurücksetzen"
}
}

View File

@@ -0,0 +1,58 @@
{
"common": {
"note": "note",
"file": "file",
"advanced": "advanced",
"create": "create",
"loading": "loading",
"mode": "mode",
"views": "{n, plural, =0 {views} =1 {1 view} other {# views}}",
"minutes": "{n, plural, =0 {minutes} =1 {1 minute} other {# minutes}}",
"max": "max",
"share_link": "share link",
"copy_clipboard": "copy to clipboard",
"copied_to_clipboard": "copied to clipboard",
"encrypting": "encrypting",
"decrypting": "decrypting",
"uploading": "uploading",
"downloading": "downloading",
"qr_code": "qr code",
"password": "password"
},
"home": {
"intro": "Easily send <i>fully encrypted</i>, secure notes or files with one click. Just create a note and share the link.",
"explanation": "the note will expire and be destroyed after {type}.",
"new_note": "new note",
"new_note_notice": "<b>availability:</b><br />the note is not guaranteed to be stored as everything is kept in ram, if it fills up the oldest notes will be removed.<br />(you probably will be fine, just be warned.)",
"errors": {
"note_to_big": "could not create note. note is too big",
"note_error": "could not create note. please try again.",
"max": "max: {n}",
"empty_content": "note is empty."
},
"messages": {
"note_created": "note created."
},
"advanced": {
"explanation": "By default, a securely generated password is used for each note. You can however also choose your own password, which is not included in the link.",
"custom_password": "custom password"
}
},
"show": {
"errors": {
"not_found": "note was not found or was already deleted.",
"decryption_failed": "wrong password. could not decipher. probably a broken link. note was destroyed.",
"unsupported_type": "unsupported note type."
},
"explanation": "click below to show and delete the note if the counter has reached its limit",
"show_note": "show note",
"warning_will_not_see_again": "you will <b>not</b> get the chance to see the note again.",
"download_all": "download all",
"links_found": "links found inside the note:"
},
"file_upload": {
"selected_files": "Selected Files",
"no_files_selected": "No Files Selected",
"clear": "Reset"
}
}

Some files were not shown because too many files have changed in this diff Show More