Compare commits

..

36 Commits

Author SHA1 Message Date
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
55 changed files with 1300 additions and 1135 deletions

2
.dev.env Normal file
View File

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

View File

@@ -1,15 +1,15 @@
* *
!/backend/src !/packages/backend/src
!/backend/Cargo.lock !/packages/backend/Cargo.lock
!/backend/Cargo.toml !/packages/backend/Cargo.toml
!/frontend/locales !/packages/frontend/locales
!/frontend/src !/packages/frontend/src
!/frontend/static !/packages/frontend/static
!/frontend/.npmrc !/packages/frontend/.npmrc
!/frontend/package.json !/packages/frontend/package.json
!/frontend/pnpm-lock.yaml !/packages/frontend/pnpm-lock.yaml
!/frontend/svelte.config.js !/packages/frontend/svelte.config.js
!/frontend/tsconfig.json !/packages/frontend/tsconfig.json
!/frontend/vite.config.js !/packages/frontend/vite.config.js

View File

@@ -1,4 +1,4 @@
name: ci name: Publish
on: on:
workflow_dispatch: workflow_dispatch:
@@ -31,11 +31,8 @@ jobs:
username: ${{ secrets.DOCKERHUB_USERNAME }} username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }} password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push - name: Build and push
id: docker_build
uses: docker/build-push-action@v2 uses: docker/build-push-action@v2
with: with:
platforms: linux/amd64,linux/arm64 platforms: linux/amd64,linux/arm64
push: true push: true
tags: ${{ steps.meta.outputs.tags }} tags: ${{ steps.meta.outputs.tags }}
- name: Image digest
run: echo ${{ steps.docker_build.outputs.digest }}

View File

@@ -1,36 +1,36 @@
name: Test
on: on:
push:
branches:
- main
pull_request: pull_request:
jobs: jobs:
test: test:
runs-on: ubuntu-latest runs-on: ubuntu-latest
services:
redis:
image: redis:7-alpine
ports:
- 6379:6379
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- uses: actions/setup-node@v2 - uses: actions/setup-node@v2
with: with:
node-version: "16" node-version: "16"
- uses: pnpm/action-setup@v2
- uses: docker/setup-qemu-action@v1
- uses: docker/setup-buildx-action@v1
with: with:
version: 7 install: true
- uses: actions-rs/toolchain@v1 - name: Build docker image
with: run: npm run test:prepare
toolchain: 1.61
- name: Prepare - name: Prepare
run: | run: |
pnpm install npm install playwright
pnpm run test:prepare npx playwright install --with-deps
- name: Install Playwright
run: npx playwright install --with-deps
- name: Run your tests - name: Run your tests
run: pnpm run test:run run: npm test
- name: Upload test results - uses: actions/upload-artifact@v2
if: always() if: always()
uses: actions/upload-artifact@v2
with: with:
name: test-results name: test-results
path: test-results path: test-results

View File

@@ -1,6 +0,0 @@
{
"cSpell.words": ["ciphertext", "cryptgeon"],
"i18n-ally.localesPaths": ["packages/frontend/locales"],
"i18n-ally.enabledFrameworks": ["svelte"],
"i18n-ally.keystyle": "nested"
}

View File

@@ -5,6 +5,56 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [2.2.0] - 2023-01-14
### 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 ## [2.0.3] - 2022-10-07
### Added ### Added
@@ -21,7 +71,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Toasts for events. - Toasts for events.
- E2E Tests. - E2E Tests.
- Make backend more configurable - Make backend more configurable.
## [2.0.1] - 2022-07-18 ## [2.0.1] - 2022-07-18

View File

@@ -9,13 +9,16 @@ RUN pnpm run build
# BACKEND # BACKEND
FROM rust:1.61-alpine as backend FROM rust:1.64-alpine as backend
WORKDIR /tmp WORKDIR /tmp
RUN apk add libc-dev openssl-dev alpine-sdk RUN apk add libc-dev openssl-dev alpine-sdk
COPY ./packages/backend/Cargo.* ./ COPY ./packages/backend/Cargo.* ./
RUN cargo fetch # https://blog.rust-lang.org/2022/06/22/sparse-registry-testing.html
RUN rustup update nightly
ENV CARGO_UNSTABLE_SPARSE_REGISTRY=true
RUN cargo +nightly fetch
COPY ./packages/backend ./ COPY ./packages/backend ./
RUN cargo build --release RUN cargo +nightly build --release
# RUNNER # RUNNER
@@ -25,5 +28,5 @@ COPY --from=backend /tmp/target/release/cryptgeon .
COPY --from=client /tmp/build ./frontend COPY --from=client /tmp/build ./frontend
ENV FRONTEND_PATH="./frontend" ENV FRONTEND_PATH="./frontend"
ENV REDIS="redis://redis/" ENV REDIS="redis://redis/"
EXPOSE 5000 EXPOSE 8000
ENTRYPOINT [ "/app/cryptgeon" ] ENTRYPOINT [ "/app/cryptgeon" ]

View File

@@ -24,9 +24,9 @@ _cryptgeon_ is a secure, open source sharing note or file service inspired by [_
> >
> Thanks to [Lokalise](https://lokalise.com/) for providing free access to their platform. > Thanks to [Lokalise](https://lokalise.com/) for providing free access to their platform.
## Demo ## Live Service / Demo
Check out the demo and see for yourself [cryptgeon.org](https://cryptgeon.org) Check out the live service / demo and see for yourself [cryptgeon.org](https://cryptgeon.org)
## Features ## Features
@@ -50,16 +50,18 @@ of the notes even if it tried to.
## Environment Variables ## Environment Variables
| Variable | Default | Description | | Variable | Default | Description |
| ---------------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | ------------------ | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `REDIS` | `redis://redis/` | Redis URL to connect to. [According to format](https://docs.rs/redis/latest/redis/#connection-parameters) | | `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. | | `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_VIEWS` | `100` | Maximal number of views. |
| `MAX_EXPIRATION` | `360` | Maximal expiration in minutes. | | `MAX_EXPIRATION` | `360` | Maximal expiration in minutes. |
| `ALLOW_ADVANCED` | `true` | Allow custom configuration. If set to `false` all notes will be one view only. | | `ALLOW_ADVANCED` | `true` | Allow custom configuration. If set to `false` all notes will be one view only. |
| `THEME_IMAGE` | `""` | Custom image for replacing the logo. Must be publicly reachable | | `VERBOSITY` | `warn` | Verbosity level for the backend. [Possible values](https://docs.rs/env_logger/latest/env_logger/#enabling-logging) are: `error`, `warn`, `info`, `debug`, `trace` |
| `THEME_TEXT` | `""` | Custom text for replacing the description below the logo | | `THEME_IMAGE` | `""` | Custom image for replacing the logo. Must be publicly reachable |
| `VERBOSITY` | `warn` | Verbosity level for the backend. [Possible values](https://docs.rs/env_logger/latest/env_logger/#enabling-logging) are: `error`, `warn`, `info`, `debug`, `trace` | | `THEME_TEXT` | `""` | Custom text for replacing the description below the logo |
| `THEME_PAGE_TITLE` | `""` | Custom text the page title |
| `THEME_FAVICON` | `""` | Custom url for the favicon. Must be publicly reachable |
## Deployment ## Deployment
@@ -77,15 +79,19 @@ version: '3.8'
services: services:
redis: redis:
image: redis:7-alpine image: redis:7-alpine
# Set a size limit. See link below on how to customise.
# https://redis.io/docs/manual/eviction/
command: redis-server --maxmemory 1gb --maxmemory-policy allkeys-lru
app: app:
image: cupcakearmy/cryptgeon:latest image: cupcakearmy/cryptgeon:latest
depends_on: depends_on:
- redis - redis
environment: environment:
# Size limit for a single note.
SIZE_LIMIT: 4 MiB SIZE_LIMIT: 4 MiB
ports: ports:
- 80:5000 - 80:8000
``` ```
### NGINX Proxy ### NGINX Proxy
@@ -104,6 +110,11 @@ See the [examples/scratch](https://github.com/cupcakearmy/cryptgeon/tree/main/ex
There is a [guide](https://mariushosting.com/how-to-install-cryptgeon-on-your-synology-nas/) you can follow. There is a [guide](https://mariushosting.com/how-to-install-cryptgeon-on-your-synology-nas/) you can follow.
### YouTube Guides
- 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)
## Development ## Development
**Requirements** **Requirements**
@@ -127,9 +138,6 @@ cargo install cargo-watch
Make sure you have docker running. Make sure you have docker running.
> If you are on `macOS` you might need to disable AirPlay Receiver as it uses port 5000 (So stupid...)
> https://developer.apple.com/forums/thread/682332
```bash ```bash
pnpm run dev pnpm run dev
``` ```

View File

@@ -82,7 +82,7 @@ services:
environment: environment:
SIZE_LIMIT: 4 MiB SIZE_LIMIT: 4 MiB
ports: ports:
- 80:5000 - 80:8000
``` ```
### NGINX 反向代理 ### NGINX 反向代理
@@ -148,9 +148,6 @@ cargo install cargo-watch
确保你的 Docker 正在运行 确保你的 Docker 正在运行
> 如果你用的是 `macOS` 的话你可能需要关闭 AirPlay 接收功能因为该功能需要占用 5000 端口...)
> https://developer.apple.com/forums/thread/682332
```bash ```bash
pnpm run dev pnpm run dev
``` ```

16
cryptgeon.code-workspace Normal file
View File

@@ -0,0 +1,16 @@
{
"folders": [
{
"path": "."
},
{
"path": "packages/backend"
}
],
"settings": {
"cSpell.words": ["ciphertext", "cryptgeon"],
"i18n-ally.enabledFrameworks": ["svelte"],
"i18n-ally.keystyle": "nested",
"i18n-ally.localesPaths": ["packages/frontend/locales"]
}
}

View File

@@ -10,11 +10,10 @@ services:
- 6379:6379 - 6379:6379
app: app:
# build: . image: cupcakearmy/cryptgeon:test
image: cupcakearmy/cryptgeon build: .
env_file: .dev.env
depends_on: depends_on:
- redis - redis
environment:
SIZE_LIMIT: 10 MiB
ports: ports:
- 1234:5000 - 1234:8000

18
docker-compose.yaml Normal file
View File

@@ -0,0 +1,18 @@
version: '3.8'
services:
redis:
image: redis:7-alpine
# Set a size limit. See link below on how to customise.
# https://redis.io/docs/manual/eviction/
# command: redis-server --maxmemory 1gb --maxmemory-policy allkeys-lru
app:
image: cupcakearmy/cryptgeon:latest
depends_on:
- redis
environment:
# Size limit for a single note.
SIZE_LIMIT: 4 MiB
ports:
- 80:8000

View File

@@ -4,7 +4,7 @@ server {
server_name _; server_name _;
location / { location / {
proxy_pass http://app:5000/; proxy_pass http://app:8000/;
proxy_set_header Host $host; proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

View File

@@ -20,7 +20,7 @@ server {
ssl_trusted_certificate /path/to/fullchain.pem; ssl_trusted_certificate /path/to/fullchain.pem;
location / { location / {
proxy_pass http://app:5000/; proxy_pass http://app:8000/;
proxy_set_header Host $host; proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

View File

@@ -1,17 +1,17 @@
{ {
"scripts": { "scripts": {
"dev:docker": "docker-compose up redis", "dev:docker": "docker-compose -f docker-compose.dev.yaml up redis",
"dev:packages": "pnpm --parallel run dev", "dev:packages": "pnpm --parallel run dev",
"dev:proxy": "node proxy.mjs", "dev:proxy": "node proxy.mjs",
"dev": "run-p dev:*", "dev": "run-p dev:*",
"test:run": "playwright test --project chrome firefox safari", "test": "playwright test --project chrome firefox safari",
"test:local": "playwright test --project local", "test:local": "playwright test --project local",
"test:server": "pnpm --parallel run test:server", "test:server": "docker compose -f docker-compose.dev.yaml up",
"test:prepare": "pnpm --parallel run test:prepare" "test:prepare": "docker compose -f docker-compose.dev.yaml build"
}, },
"devDependencies": { "devDependencies": {
"@playwright/test": "^1.25.1", "@playwright/test": "^1.29.2",
"@types/node": "^16.11.57", "@types/node": "^16.18.11",
"http-proxy": "^1.18.1", "http-proxy": "^1.18.1",
"npm-run-all": "^4.1.5" "npm-run-all": "^4.1.5"
} }

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "cryptgeon" name = "cryptgeon"
version = "2.0.3" version = "2.2.0"
authors = ["cupcakearmy <hi@nicco.io>"] authors = ["cupcakearmy <hi@nicco.io>"]
edition = "2021" edition = "2021"

View File

@@ -8,7 +8,7 @@ lazy_static! {
pub static ref FRONTEND_PATH: String = pub static ref FRONTEND_PATH: String =
std::env::var("FRONTEND_PATH").unwrap_or("../frontend/build".to_string()); std::env::var("FRONTEND_PATH").unwrap_or("../frontend/build".to_string());
pub static ref LISTEN_ADDR: String = pub static ref LISTEN_ADDR: String =
std::env::var("LISTEN_ADDR").unwrap_or("0.0.0.0:5000".to_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()); pub static ref VERBOSITY: String = std::env::var("VERBOSITY").unwrap_or("warn".to_string());
} }
@@ -42,4 +42,12 @@ lazy_static! {
.unwrap_or("".to_string()) .unwrap_or("".to_string())
.parse() .parse()
.unwrap(); .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();
} }

View File

@@ -49,7 +49,7 @@ async fn create(note: web::Json<Note>) -> impl Responder {
} }
match n.views { match n.views {
Some(v) => { Some(v) => {
if v > *config::MAX_VIEWS { if v > *config::MAX_VIEWS || v < 1 {
return bad_req; return bad_req;
} }
n.expiration = None; // views overrides expiration n.expiration = None; // views overrides expiration

View File

@@ -12,4 +12,6 @@ pub struct Status {
// Theme // Theme
pub theme_image: String, pub theme_image: String,
pub theme_text: String, pub theme_text: String,
pub theme_page_title: String,
pub theme_favicon: String,
} }

View File

@@ -13,6 +13,8 @@ async fn get_status() -> impl Responder {
allow_advanced: *config::ALLOW_ADVANCED, allow_advanced: *config::ALLOW_ADVANCED,
theme_image: config::THEME_IMAGE.to_string(), theme_image: config::THEME_IMAGE.to_string(),
theme_text: config::THEME_TEXT.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()
}); });
} }

View File

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

View File

@@ -15,7 +15,8 @@
"encrypting": "verschlüsseln", "encrypting": "verschlüsseln",
"decrypting": "entschlüsselt", "decrypting": "entschlüsselt",
"uploading": "hochladen", "uploading": "hochladen",
"downloading": "wird heruntergeladen" "downloading": "wird heruntergeladen",
"qr_code": "qr-code"
}, },
"home": { "home": {
"intro": "Senden Sie ganz einfach <i>vollständig verschlüsselte</i>, sichere Notizen oder Dateien mit einem Klick. Erstellen Sie einfach eine Notiz und teilen Sie den Link.", "intro": "Senden Sie ganz einfach <i>vollständig verschlüsselte</i>, sichere Notizen oder Dateien mit einem Klick. Erstellen Sie einfach eine Notiz und teilen Sie den Link.",

View File

@@ -15,7 +15,8 @@
"encrypting": "encrypting", "encrypting": "encrypting",
"decrypting": "decrypting", "decrypting": "decrypting",
"uploading": "uploading", "uploading": "uploading",
"downloading": "downloading" "downloading": "downloading",
"qr_code": "qr code"
}, },
"home": { "home": {
"intro": "Easily send <i>fully encrypted</i>, secure notes or files with one click. Just create a note and share the link.", "intro": "Easily send <i>fully encrypted</i>, secure notes or files with one click. Just create a note and share the link.",

View File

@@ -15,7 +15,8 @@
"encrypting": "encriptando", "encrypting": "encriptando",
"decrypting": "descifrando", "decrypting": "descifrando",
"uploading": "cargando", "uploading": "cargando",
"downloading": "descargando" "downloading": "descargando",
"qr_code": "código qr"
}, },
"home": { "home": {
"intro": "Envía fácilmente notas o archivos <i>totalmente encriptados</i> y seguros con un solo clic. Solo tienes que crear una nota y compartir el enlace.", "intro": "Envía fácilmente notas o archivos <i>totalmente encriptados</i> y seguros con un solo clic. Solo tienes que crear una nota y compartir el enlace.",

View File

@@ -15,7 +15,8 @@
"encrypting": "cryptage", "encrypting": "cryptage",
"decrypting": "déchiffrer", "decrypting": "déchiffrer",
"uploading": "téléchargement", "uploading": "téléchargement",
"downloading": "téléchargement" "downloading": "téléchargement",
"qr_code": "code qr"
}, },
"home": { "home": {
"intro": "Envoyez facilement des notes ou des fichiers <i>entièrement cryptés</i> et sécurisés en un seul clic. Il suffit de créer une note et de partager le lien.", "intro": "Envoyez facilement des notes ou des fichiers <i>entièrement cryptés</i> et sécurisés en un seul clic. Il suffit de créer une note et de partager le lien.",

View File

@@ -15,7 +15,8 @@
"encrypting": "criptando", "encrypting": "criptando",
"decrypting": "decifrando", "decrypting": "decifrando",
"uploading": "caricamento", "uploading": "caricamento",
"downloading": "scaricando" "downloading": "scaricando",
"qr_code": "codice qr"
}, },
"home": { "home": {
"intro": "Invia facilmente note o file <i>completamente criptati</i> e sicuri con un solo clic. Basta creare una nota e condividere il link.", "intro": "Invia facilmente note o file <i>completamente criptati</i> e sicuri con un solo clic. Basta creare una nota e condividere il link.",

View File

@@ -15,7 +15,8 @@
"encrypting": "加密", "encrypting": "加密",
"decrypting": "解密", "decrypting": "解密",
"uploading": "上传", "uploading": "上传",
"downloading": "下载" "downloading": "下载",
"qr_code": "二维码"
}, },
"home": { "home": {
"intro": "一键轻松发送 <i>完全加密的</i> 密信或者文件。只需创建一个密信然后分享链接。", "intro": "一键轻松发送 <i>完全加密的</i> 密信或者文件。只需创建一个密信然后分享链接。",

View File

@@ -3,7 +3,7 @@
"scripts": { "scripts": {
"dev": "vite dev", "dev": "vite dev",
"build": "vite build", "build": "vite build",
"preview": "vite preview --port 3000", "preview": "vite preview",
"check": "svelte-check --tsconfig tsconfig.json", "check": "svelte-check --tsconfig tsconfig.json",
"licenses": "license-checker --summary > licenses.csv", "licenses": "license-checker --summary > licenses.csv",
"locale:download": "node scripts/locale.js", "locale:download": "node scripts/locale.js",
@@ -11,27 +11,29 @@
}, },
"type": "module", "type": "module",
"devDependencies": { "devDependencies": {
"@lokalise/node-api": "^7.3.1", "@lokalise/node-api": "^9.5.0",
"@sveltejs/adapter-static": "1.0.0-next.42", "@sveltejs/adapter-static": "^1.0.2",
"@sveltejs/kit": "1.0.0-next.480", "@sveltejs/kit": "^1.1.0",
"@types/dompurify": "^2.3.3", "@types/dompurify": "^2.4.0",
"@types/file-saver": "^2.0.5", "@types/file-saver": "^2.0.5",
"@zerodevx/svelte-toast": "^0.7.2", "@zerodevx/svelte-toast": "^0.7.2",
"adm-zip": "^0.5.9", "adm-zip": "^0.5.10",
"dotenv": "^16.0.1", "dotenv": "^16.0.3",
"svelte": "^3.49.0", "svelte": "^3.55.1",
"svelte-check": "^2.8.0", "svelte-check": "^2.10.3",
"svelte-intl-precompile": "^0.10.1", "svelte-intl-precompile": "^0.10.1",
"svelte-preprocess": "^4.10.7", "svelte-preprocess": "^4.10.7",
"tslib": "^2.4.0", "tslib": "^2.4.1",
"typescript": "^4.7.4", "typescript": "^4.9.4",
"vite": "^3.0.2" "vite": "^4.0.4"
}, },
"dependencies": { "dependencies": {
"@fontsource/fira-mono": "^4.5.8", "@fontsource/fira-mono": "^4.5.10",
"copy-to-clipboard": "^3.3.1", "copy-to-clipboard": "^3.3.3",
"dompurify": "^2.3.10", "dompurify": "^2.4.3",
"file-saver": "^2.0.5", "file-saver": "^2.0.5",
"pretty-bytes": "^5.6.0" "occulto": "2.0.0",
"pretty-bytes": "^6.0.0",
"qrious": "^4.0.2"
} }
} }

View File

@@ -1,7 +1,7 @@
import dotenv from 'dotenv'
import { LokaliseApi } from '@lokalise/node-api' import { LokaliseApi } from '@lokalise/node-api'
import https from 'https'
import AdmZip from 'adm-zip' import AdmZip from 'adm-zip'
import dotenv from 'dotenv'
import https from 'https'
dotenv.config() dotenv.config()

View File

@@ -87,6 +87,8 @@ button {
font-size: inherit; font-size: inherit;
background: inherit; background: inherit;
color: inherit; color: inherit;
border: none;
padding-inline: initial;
} }
*:disabled, *:disabled,

View File

@@ -2,7 +2,6 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<link rel="icon" href="/favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
%sveltekit.head% %sveltekit.head%

View File

@@ -1,34 +1,33 @@
import { AES, Bytes, type TypedArray } from 'occulto'
import type { EncryptedFileDTO, FileDTO } from './api' import type { EncryptedFileDTO, FileDTO } from './api'
import { Crypto } from './crypto'
abstract class CryptAdapter<T> { abstract class CryptAdapter<T> {
abstract encrypt(plaintext: T, key: CryptoKey): Promise<string> abstract encrypt(plaintext: T, key: TypedArray): Promise<string>
abstract decrypt(ciphertext: string, key: CryptoKey): Promise<T> abstract decrypt(ciphertext: string, key: TypedArray): Promise<T>
} }
class CryptTextAdapter implements CryptAdapter<string> { class CryptTextAdapter implements CryptAdapter<string> {
async encrypt(plaintext: string, key: CryptoKey) { async encrypt(plaintext: string, key: TypedArray) {
return await Crypto.encrypt(new TextEncoder().encode(plaintext), key) return await AES.encrypt(Bytes.encode(plaintext), key)
} }
async decrypt(ciphertext: string, key: CryptoKey) { async decrypt(ciphertext: string, key: TypedArray) {
const plaintext = await Crypto.decrypt(ciphertext, key) return Bytes.decode(await AES.decrypt(ciphertext, key))
return new TextDecoder().decode(plaintext)
} }
} }
class CryptBlobAdapter implements CryptAdapter<Blob> { class CryptBlobAdapter implements CryptAdapter<Blob> {
async encrypt(plaintext: Blob, key: CryptoKey) { async encrypt(plaintext: Blob, key: TypedArray) {
return await Crypto.encrypt(await plaintext.arrayBuffer(), key) return await AES.encrypt(new Uint8Array(await plaintext.arrayBuffer()), key)
} }
async decrypt(ciphertext: string, key: CryptoKey) { async decrypt(ciphertext: string, key: TypedArray) {
const plaintext = await Crypto.decrypt(ciphertext, key) const plaintext = await AES.decrypt(ciphertext, key)
return new Blob([plaintext], { type: 'application/octet-stream' }) return new Blob([plaintext], { type: 'application/octet-stream' })
} }
} }
class CryptFilesAdapter implements CryptAdapter<FileDTO[]> { class CryptFilesAdapter implements CryptAdapter<FileDTO[]> {
async encrypt(plaintext: FileDTO[], key: CryptoKey) { async encrypt(plaintext: FileDTO[], key: TypedArray) {
const adapter = new CryptBlobAdapter() const adapter = new CryptBlobAdapter()
const data: Promise<EncryptedFileDTO>[] = plaintext.map(async (file) => ({ const data: Promise<EncryptedFileDTO>[] = plaintext.map(async (file) => ({
name: file.name, name: file.name,
@@ -39,7 +38,7 @@ class CryptFilesAdapter implements CryptAdapter<FileDTO[]> {
return JSON.stringify(await Promise.all(data)) return JSON.stringify(await Promise.all(data))
} }
async decrypt(ciphertext: string, key: CryptoKey) { async decrypt(ciphertext: string, key: TypedArray) {
const adapter = new CryptBlobAdapter() const adapter = new CryptBlobAdapter()
const data: EncryptedFileDTO[] = JSON.parse(ciphertext) const data: EncryptedFileDTO[] = JSON.parse(ciphertext)
const files: FileDTO[] = await Promise.all( const files: FileDTO[] = await Promise.all(

View File

@@ -1,89 +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
}
}
export class ArrayBufferUtils {
static async toString(buffer: ArrayBuffer): Promise<string> {
const reader = new window.FileReader()
reader.readAsDataURL(new Blob([buffer]))
return new Promise((resolve) => {
reader.onloadend = () => resolve(reader.result as string)
})
}
static async fromString(s: string): Promise<ArrayBuffer> {
return fetch(s)
.then((r) => r.blob())
.then((b) => b.arrayBuffer())
}
}
export class Keys {
public static async generateKey(size: 128 | 192 | 256 = 256): Promise<CryptoKey> {
const key = await window.crypto.subtle.generateKey(
{
name: 'AES-GCM',
length: size,
},
true,
['encrypt', 'decrypt']
)
return key
}
public static async export(key: CryptoKey): Promise<string> {
return Hex.encode(await window.crypto.subtle.exportKey('raw', key))
}
public static async import(key: string): Promise<CryptoKey> {
return window.crypto.subtle.importKey('raw', Hex.decode(key), { name: 'AES-GCM' }, true, [
'encrypt',
'decrypt',
])
}
}
export class Crypto {
private static ALG = 'AES-GCM'
private static DELIMITER = ':::'
public static getRandomBytes(size: number): Uint8Array {
return window.crypto.getRandomValues(new Uint8Array(size))
}
public static async encrypt(plaintext: ArrayBuffer, key: CryptoKey): Promise<string> {
const iv = this.getRandomBytes(12) // AES-GCM needs a 96bit IV
const encrypted: ArrayBuffer = await window.crypto.subtle.encrypt(
{ name: this.ALG, iv },
key,
plaintext
)
const data = [Hex.encode(iv), await ArrayBufferUtils.toString(encrypted)].join(this.DELIMITER)
return data
}
public static async decrypt(ciphertext: string, key: CryptoKey): Promise<ArrayBuffer> {
const splitted = ciphertext.split(this.DELIMITER)
const iv = Hex.decode(splitted[0])
const encrypted = await ArrayBufferUtils.fromString(splitted[1])
const plaintext = await window.crypto.subtle.decrypt({ name: this.ALG, iv }, key, encrypted)
return plaintext
}
}

View File

@@ -9,6 +9,8 @@ export type Status = {
allow_advanced: boolean allow_advanced: boolean
theme_image: string theme_image: string
theme_text: string theme_text: string
theme_favicon: string
theme_page_title: string
} }
export const status = writable<null | Status>(null) export const status = writable<null | Status>(null)

View File

@@ -18,8 +18,9 @@
bind:value={note.views} bind:value={note.views}
disabled={timeExpiration} disabled={timeExpiration}
max={$status?.max_views} max={$status?.max_views}
min={1}
validate={(v) => validate={(v) =>
($status && v <= $status?.max_views) || ($status && v <= $status?.max_views && v > 0) ||
$t('home.errors.max', { values: { n: $status?.max_views ?? 0 } })} $t('home.errors.max', { values: { n: $status?.max_views ?? 0 } })}
/> />
<div class="middle-switch"> <div class="middle-switch">

View File

@@ -0,0 +1,41 @@
<script lang="ts">
import QR from 'qrious'
import { t } from 'svelte-intl-precompile'
import { getCSSVariable } from '$lib/utils'
export let value: string
let canvas: HTMLCanvasElement
$: {
new QR({
value,
level: 'Q',
size: 800,
background: getCSSVariable('--ui-bg-0'),
foreground: getCSSVariable('--ui-text-0'),
element: canvas,
})
}
</script>
<small>{$t('common.qr_code')}</small>
<div>
<canvas bind:this={canvas} />
</div>
<style>
div {
padding: 0.5rem;
width: fit-content;
border: 2px solid var(--ui-bg-1);
background-color: var(--ui-bg-0);
margin-top: 0.125rem;
}
canvas {
width: 100%;
height: auto;
}
</style>

View File

@@ -10,9 +10,12 @@
import Button from '$lib/ui/Button.svelte' import Button from '$lib/ui/Button.svelte'
import TextInput from '$lib/ui/TextInput.svelte' import TextInput from '$lib/ui/TextInput.svelte'
import Canvas from './Canvas.svelte'
export let result: NoteResult export let result: NoteResult
$: url = `${window.location.origin}/note/${result.id}#${result.password}`
function reset() { function reset() {
window.location.reload() window.location.reload()
} }
@@ -22,11 +25,15 @@
type="text" type="text"
readonly readonly
label={$t('common.share_link')} label={$t('common.share_link')}
value="{window.location.origin}/note/{result.id}#{result.password}" value={url}
copy copy
data-testid="share-link" data-testid="share-link"
/> />
<br />
<div>
<Canvas value={url} />
</div>
<p> <p>
{@html $t('home.new_note_notice')} {@html $t('home.new_note_notice')}
</p> </p>
@@ -34,4 +41,9 @@
<Button on:click={reset}>{$t('home.new_note')}</Button> <Button on:click={reset}>{$t('home.new_note')}</Button>
<style> <style>
div {
width: min(12rem, 100%);
margin-top: 1rem;
margin-bottom: 1rem;
}
</style> </style>

View File

@@ -53,7 +53,9 @@
{:else} {:else}
{#each files as file} {#each files as file}
<div class="note file"> <div class="note file">
<b on:click={() => downloadFile(file)}> {file.name}</b> <button on:click={() => downloadFile(file)}>
<b>{file.name}</b>
</button>
<small> {file.type} {prettyBytes(file.size)}</small> <small> {file.type} {prettyBytes(file.size)}</small>
</div> </div>
{/each} {/each}

View File

@@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import { Crypto, Hex } from '$lib/crypto'
import Icon from '$lib/ui/Icon.svelte' import Icon from '$lib/ui/Icon.svelte'
import { copy as copyFN } from '$lib/utils' import { copy as copyFN } from '$lib/utils'
import { getRandomBytes, Hex } from 'occulto'
export let label: string = '' export let label: string = ''
export let value: any export let value: any
@@ -23,8 +23,9 @@
function toggle() { function toggle() {
hidden = !hidden hidden = !hidden
} }
function randomFN() {
value = Hex.encode(Crypto.getRandomBytes(32)) async function randomFN() {
value = Hex.encode(await getRandomBytes(32))
} }
</script> </script>

View File

@@ -40,19 +40,19 @@
} }
</script> </script>
<div on:click={change}> <button on:click={change}>
<Icon class="icon" icon="contrast" /> <Icon class="icon" icon="contrast" />
{$theme} {$theme}
</div> </button>
<style> <style>
div :global(.icon) { button :global(.icon) {
height: 1rem; height: 1rem;
width: 1rem; width: 1rem;
margin-right: 0.5rem; margin-right: 0.5rem;
} }
div { button {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
justify-content: flex-end; justify-content: flex-end;

View File

@@ -9,3 +9,8 @@ export function copy(value: string) {
const msg = get(t)('common.copied_to_clipboard') const msg = get(t)('common.copied_to_clipboard')
notify.success(msg) notify.success(msg)
} }
export function getCSSVariable(variable: string): string {
if (typeof window === 'undefined') return ''
return window.getComputedStyle(window.document.body).getPropertyValue(variable)
}

View File

@@ -1,11 +1,11 @@
<script lang="ts"> <script lang="ts">
import { AES, Hex } from 'occulto'
import { t } from 'svelte-intl-precompile' import { t } from 'svelte-intl-precompile'
import { blur } from 'svelte/transition' import { blur } from 'svelte/transition'
import { Adapters } from '$lib/adapters' import { Adapters } from '$lib/adapters'
import type { FileDTO, Note } from '$lib/api' import type { FileDTO, Note } from '$lib/api'
import { create, PayloadToLargeError } from '$lib/api' import { create, PayloadToLargeError } from '$lib/api'
import { Keys } from '$lib/crypto'
import { status } from '$lib/stores/status' import { status } from '$lib/stores/status'
import { notify } from '$lib/toast' import { notify } from '$lib/toast'
import AdvancedParameters from '$lib/ui/AdvancedParameters.svelte' import AdvancedParameters from '$lib/ui/AdvancedParameters.svelte'
@@ -58,8 +58,8 @@
try { try {
loading = $t('common.encrypting') loading = $t('common.encrypting')
const key = await Keys.generateKey() const key = await AES.generateKey()
const password = await Keys.export(key) const password = await Hex.encode(key)
const data: Note = { const data: Note = {
contents: '', contents: '',

View File

@@ -7,7 +7,9 @@
<nav> <nav>
<a href="/">/home</a> <a href="/">/home</a>
<a href="/about">/about</a> <a href="/about">/about</a>
<a href="https://github.com/cupcakearmy/cryptgeon" target="_blank" rel="noopener">/code</a> <a href="https://github.com/cupcakearmy/cryptgeon" target="_blank" rel="noopener noreferrer">
code
</a>
</nav> </nav>
</footer> </footer>

View File

@@ -5,7 +5,7 @@
import '../app.css' import '../app.css'
import { init as initStores } from '$lib/stores/status' import { init as initStores, status } from '$lib/stores/status'
import Footer from '$lib/views/Footer.svelte' import Footer from '$lib/views/Footer.svelte'
import Header from '$lib/views/Header.svelte' import Header from '$lib/views/Header.svelte'
@@ -15,7 +15,8 @@
</script> </script>
<svelte:head> <svelte:head>
<title>cryptgeon</title> <title>{$status?.theme_page_title || 'cryptgeon'}</title>
<link rel="icon" href={$status?.theme_favicon || '/favicon.png'} />
</svelte:head> </svelte:head>
{#await waitLocale() then _} {#await waitLocale() then _}

View File

@@ -39,7 +39,7 @@
the backend is written in rust and the frontend is svelte and typescript. the backend is written in rust and the frontend is svelte and typescript.
<br /> <br />
you are welcomed to check & audit the you are welcomed to check & audit the
<a href="https://github.com/cupcakearmy/cryptgeon" target="_blank" rel="noopener"> <a href="https://github.com/cupcakearmy/cryptgeon" target="_blank" rel="noopener noreferrer">
source code source code
</a>. </a>.
</span> </span>
@@ -47,9 +47,12 @@
<AboutParagraph title="translations"> <AboutParagraph title="translations">
<span <span
>translations are managed on <a href="https://lokalise.com/" target="_blank">Lokalise</a>, >translations are managed on <a
which granted an open source license to use the paid version. If you are interested in helping href="https://lokalise.com/"
translating don't hesitate to contact me! target="_blank"
rel="noopener noreferrer">Lokalise</a
>, which granted an open source license to use the paid version. If you are interested in
helping translating don't hesitate to contact me!
</span> </span>
</AboutParagraph> </AboutParagraph>

View File

@@ -1,10 +1,10 @@
<script lang="ts"> <script lang="ts">
import { Hex } from 'occulto'
import { onMount } from 'svelte' import { onMount } from 'svelte'
import { t } from 'svelte-intl-precompile' import { t } from 'svelte-intl-precompile'
import { Adapters } from '$lib/adapters' import { Adapters } from '$lib/adapters'
import { get, info } from '$lib/api' import { get, info } from '$lib/api'
import { Keys } from '$lib/crypto'
import Button from '$lib/ui/Button.svelte' import Button from '$lib/ui/Button.svelte'
import Loader from '$lib/ui/Loader.svelte' import Loader from '$lib/ui/Loader.svelte'
import ShowNote, { type DecryptedNote } from '$lib/ui/ShowNote.svelte' import ShowNote, { type DecryptedNote } from '$lib/ui/ShowNote.svelte'
@@ -43,7 +43,7 @@
loading = $t('common.downloading') loading = $t('common.downloading')
const data = await get(id) const data = await get(id)
loading = $t('common.decrypting') loading = $t('common.decrypting')
const key = await Keys.import(password) const key = await Hex.decode(password)
switch (data.meta.type) { switch (data.meta.type) {
case 'text': case 'text':
note = { note = {

View File

@@ -1,12 +1,13 @@
import { sveltekit } from '@sveltejs/kit/vite' import { sveltekit } from '@sveltejs/kit/vite'
import precompileIntl from 'svelte-intl-precompile/sveltekit-plugin' import precompileIntl from 'svelte-intl-precompile/sveltekit-plugin'
const port = 8001
/** @type {import('vite').UserConfig} */ /** @type {import('vite').UserConfig} */
const config = { const config = {
clearScreen: false, clearScreen: false,
server: { server: { port },
port: 3000, preview: { port },
},
plugins: [sveltekit(), precompileIntl('locales')], plugins: [sveltekit(), precompileIntl('locales')],
} }

View File

@@ -13,7 +13,7 @@ const config: PlaywrightTestConfig = {
testIgnore: ['file/too-big.spec.ts'], testIgnore: ['file/too-big.spec.ts'],
webServer: { webServer: {
command: 'pnpm run test:server', command: 'docker compose -f docker-compose.dev.yaml up',
port: 1234, port: 1234,
reuseExistingServer: true, reuseExistingServer: true,
}, },

1386
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -3,12 +3,13 @@ import httpProxy from 'http-proxy'
const proxy = httpProxy.createProxyServer() const proxy = httpProxy.createProxyServer()
proxy.on('error', function (err, req, res) { proxy.on('error', function (err, req, res) {
console.error(err)
res.writeHead(500, { 'Content-Type': 'text/plain' }) res.writeHead(500, { 'Content-Type': 'text/plain' })
res.end('500 Internal Server Error') res.end('500 Internal Server Error')
}) })
const server = http.createServer(function (req, res) { const server = http.createServer(function (req, res) {
const target = req.url.startsWith('/api/') ? 'http://localhost:5000' : 'http://localhost:3000' const target = req.url.startsWith('/api/') ? 'http://127.0.0.1:8000' : 'http://localhost:8001'
proxy.web(req, res, { target }) proxy.web(req, res, { target })
}) })
server.listen(1234) server.listen(1234)

View File

@@ -1,5 +1,5 @@
export default { export default {
PDF: 'test/assets/AES.pdf', PDF: 'test/assets/AES.pdf',
Image: 'test/assets/alfred-kenneally-UIu4RmMxnHU-unsplash.jpg', Image: 'test/assets/image.jpg',
Zip: 'test/assets/Pigeons.zip', Zip: 'test/assets/Pigeons.zip',
} }

View File

@@ -6,6 +6,6 @@ test('multiple', async ({ page }) => {
const files = [Files.PDF, Files.Image] const files = [Files.PDF, Files.Image]
const checksums = await Promise.all(files.map(getFileChecksum)) const checksums = await Promise.all(files.map(getFileChecksum))
const link = await createNote(page, { files, views: 2 }) const link = await createNote(page, { files, views: 2 })
await checkLinkForDownload(page, link, 'alfred-kenneally', checksums[1]) await checkLinkForDownload(page, link, 'image.jpg', checksums[1])
await checkLinkForDownload(page, link, 'AES.pdf', checksums[0]) await checkLinkForDownload(page, link, 'AES.pdf', checksums[0])
}) })

View File

@@ -20,5 +20,5 @@ test('image content', async ({ page }) => {
const files = [Files.Image] const files = [Files.Image]
const checksum = await getFileChecksum(files[0]) const checksum = await getFileChecksum(files[0])
const link = await createNote(page, { files }) const link = await createNote(page, { files })
await checkLinkForDownload(page, link, 'alfred-kenneally', checksum) await checkLinkForDownload(page, link, 'image.jpg', checksum)
}) })