Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ea561f635a | |||
| 1f180a2e53 | |||
| 6b29c6f069 | |||
| 40f3559533 | |||
| ac686e1935 | |||
| 2cb984405f | |||
| 8a000ce131 | |||
| 38987efc8a | |||
| 781231e414 | |||
| bb422fdd8d |
@@ -80,15 +80,17 @@ of the notes even if it tried to.
|
||||
| `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_. |
|
||||
| `REDIS_PREFIX` | `""` | Optional prefix for all Redis keys. Useful when sharing a Redis instance with other apps via ACL namespaces. |
|
||||
| `VERBOSITY` | `warn` | Verbosity level for the backend. [Possible values](https://docs.rs/env_logger/latest/env_logger/#enabling-logging) are: `error`, `warn`, `info`, `debug`, `trace` |
|
||||
| `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`. |
|
||||
| `THEME_HOME_LINK` | `true` | Show the `/home` link in the footer. Defaults to `true`. |
|
||||
| `THEME_HOME_LINK` | `true` | Show the `/home` link in the footer. 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
|
||||
|
||||
> ℹ️ `https` is required otherwise browsers will not support the cryptographic functions.
|
||||
@@ -102,7 +104,7 @@ Docker is the easiest way. There is the [official image here](https://hub.docker
|
||||
```yaml
|
||||
# docker-compose.yml
|
||||
|
||||
version: '3.8'
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
redis:
|
||||
|
||||
@@ -252,7 +252,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "cryptgeon"
|
||||
version = "2.9.2"
|
||||
version = "2.9.3"
|
||||
dependencies = [
|
||||
"axum",
|
||||
"bs62",
|
||||
@@ -261,6 +261,7 @@ dependencies = [
|
||||
"lazy_static",
|
||||
"redis",
|
||||
"ring",
|
||||
"rmp-serde",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tokio",
|
||||
@@ -1004,6 +1005,25 @@ dependencies = [
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rmp"
|
||||
version = "0.8.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4ba8be72d372b2c9b35542551678538b562e7cf86c3315773cae48dfbfe7790c"
|
||||
dependencies = [
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rmp-serde"
|
||||
version = "1.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72f81bee8c8ef9b577d1681a70ebbc962c232461e397b22c208c43c04b67a155"
|
||||
dependencies = [
|
||||
"rmp",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustix"
|
||||
version = "1.1.4"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "cryptgeon"
|
||||
version = "2.9.2"
|
||||
version = "2.9.3"
|
||||
authors = ["cupcakearmy <hi@nicco.io>"]
|
||||
edition = "2024"
|
||||
rust-version = "1.95"
|
||||
@@ -25,3 +25,4 @@ ring = "0.17"
|
||||
bs62 = "0.1"
|
||||
byte-unit = "4"
|
||||
dotenv = "0.15"
|
||||
rmp-serde = "1.3.1"
|
||||
|
||||
@@ -34,6 +34,10 @@ pub static ref ID_LENGTH: u32 = std::env::var("ID_LENGTH")
|
||||
.unwrap_or("32".to_string())
|
||||
.parse()
|
||||
.unwrap();
|
||||
pub static ref REDIS_PREFIX: String = std::env::var("REDIS_PREFIX")
|
||||
.unwrap_or("".to_string())
|
||||
.parse()
|
||||
.unwrap();
|
||||
pub static ref ALLOW_FILES: bool = std::env::var("ALLOW_FILES")
|
||||
.unwrap_or("true".to_string())
|
||||
.parse()
|
||||
|
||||
@@ -23,6 +23,7 @@ mod csp;
|
||||
mod health;
|
||||
mod lock;
|
||||
mod note;
|
||||
mod note_v2;
|
||||
mod status;
|
||||
mod store;
|
||||
|
||||
@@ -42,7 +43,8 @@ async fn main() {
|
||||
let notes_routes = Router::new()
|
||||
.route("/", post(note::create))
|
||||
.route("/{id}", delete(note::delete))
|
||||
.route("/{id}", get(note::preview));
|
||||
.route("/{id}", get(note::preview))
|
||||
.route("/v2/", post(note_v2::create));
|
||||
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()
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
mod model;
|
||||
mod routes;
|
||||
|
||||
pub use model::*;
|
||||
pub use routes::*;
|
||||
@@ -0,0 +1,41 @@
|
||||
use bs62;
|
||||
use ring::rand::SecureRandom;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::config;
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
pub struct NoteV2Metadata {
|
||||
pub views: Option<u32>,
|
||||
pub expiration: Option<u32>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
pub struct NoteV2 {
|
||||
pub metadata: NoteV2Metadata,
|
||||
pub public: String,
|
||||
pub content: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct NoteInfoV2 {
|
||||
pub public: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct NotePublicV2 {
|
||||
pub public: String,
|
||||
pub content: String,
|
||||
}
|
||||
|
||||
pub fn generate_id_v2() -> 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;
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
use axum::{
|
||||
Json,
|
||||
body::Bytes,
|
||||
extract::Path,
|
||||
http::StatusCode,
|
||||
response::{IntoResponse, Response},
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{sync::Arc, time::SystemTime};
|
||||
|
||||
use crate::note_v2::{NoteV2, generate_id_v2};
|
||||
use crate::store;
|
||||
use crate::{config, lock::SharedState};
|
||||
|
||||
use super::NotePublicV2;
|
||||
|
||||
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(NoteInfoV2 { 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(body: Bytes) -> Response {
|
||||
let id = generate_id_v2();
|
||||
println!("{}", body.len());
|
||||
(StatusCode::OK, Json(CreateResponse { id })).into_response()
|
||||
// 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(NotePublicV2 {
|
||||
// content: changed.contents,
|
||||
// public: changed.meta,
|
||||
// }),
|
||||
// )
|
||||
// .into_response();
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
@@ -1,6 +1,7 @@
|
||||
use redis;
|
||||
use redis::Commands;
|
||||
|
||||
use crate::config;
|
||||
use crate::note::now;
|
||||
use crate::note::Note;
|
||||
|
||||
@@ -11,6 +12,10 @@ lazy_static! {
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
fn prefixed(id: &String) -> String {
|
||||
format!("{}{}", config::REDIS_PREFIX.as_str(), id)
|
||||
}
|
||||
|
||||
fn get_connection() -> Result<redis::Connection, &'static str> {
|
||||
let client =
|
||||
redis::Client::open(REDIS_CLIENT.to_string()).map_err(|_| "Unable to connect to redis")?;
|
||||
@@ -28,15 +33,16 @@ pub fn can_reach_redis() -> bool {
|
||||
}
|
||||
|
||||
pub fn set(id: &String, note: &Note) -> Result<(), &'static str> {
|
||||
let key = prefixed(id);
|
||||
let serialized = serde_json::to_string(¬e.clone()).unwrap();
|
||||
let mut conn = get_connection()?;
|
||||
|
||||
conn.set::<_, _, ()>(id, serialized)
|
||||
conn.set::<_, _, ()>(key.as_str(), serialized)
|
||||
.map_err(|_| "Unable to set note in redis")?;
|
||||
match note.expiration {
|
||||
Some(e) => {
|
||||
let seconds = e - now();
|
||||
conn.expire::<_, ()>(id, seconds as i64)
|
||||
conn.expire::<_, ()>(key.as_str(), seconds as i64)
|
||||
.map_err(|_| "Unable to set expiration on note")?
|
||||
}
|
||||
None => {}
|
||||
@@ -45,8 +51,9 @@ pub fn set(id: &String, note: &Note) -> Result<(), &'static str> {
|
||||
}
|
||||
|
||||
pub fn get(id: &String) -> Result<Option<Note>, &'static str> {
|
||||
let key = prefixed(id);
|
||||
let mut conn = get_connection()?;
|
||||
let value: Option<String> = conn.get(id).map_err(|_| "Could not load note in redis")?;
|
||||
let value: Option<String> = conn.get(key.as_str()).map_err(|_| "Could not load note in redis")?;
|
||||
match value {
|
||||
None => return Ok(None),
|
||||
Some(s) => {
|
||||
@@ -57,7 +64,8 @@ pub fn get(id: &String) -> Result<Option<Note>, &'static str> {
|
||||
}
|
||||
|
||||
pub fn del(id: &String) -> Result<(), &'static str> {
|
||||
let key = prefixed(id);
|
||||
let mut conn = get_connection()?;
|
||||
conn.del::<_, ()>(id).map_err(|_| "Unable to delete note in redis")?;
|
||||
conn.del::<_, ()>(key.as_str()).map_err(|_| "Unable to delete note in redis")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "cryptgeon",
|
||||
"version": "2.9.2",
|
||||
"version": "2.9.3",
|
||||
"homepage": "https://github.com/cupcakearmy/cryptgeon",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
"new_note": "Nová poznámka",
|
||||
"new_note_notice": "<b>Dostupnost:</b><br />Poznámka není zaručeně uchována, protože je uložena pouze v paměti RAM. Pokud se paměť zaplní, nejstarší poznámky budou automaticky smazány.<br />(Obvykle to nebývá problém, ale je dobré o tom vědět.)",
|
||||
"errors": {
|
||||
"note_to_big": "Poznámku nelze vytvořit. Je příliš velká.",
|
||||
"note_too_big": "Poznámku nelze vytvořit. Je příliš velká.",
|
||||
"note_error": "Poznámku nelze vytvořit. Zkuste to prosím znovu.",
|
||||
"max": "Max: {n}",
|
||||
"empty_content": "Poznámka je prázdná."
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
"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_too_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."
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
"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_too_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."
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
"new_note": "nueva nota",
|
||||
"new_note_notice": "<b>disponibilidad:</b><br />no se garantiza que la nota se almacene, ya que todo se guarda en la memoria RAM, si se llena se eliminarán las notas más antiguas.<br />(probablemente estará bien, solo está advertido.)",
|
||||
"errors": {
|
||||
"note_to_big": "no se pudo crear la nota. la nota es demasiado grande",
|
||||
"note_too_big": "no se pudo crear la nota. la nota es demasiado grande",
|
||||
"note_error": "No se ha podido crear la nota. Por favor, inténtelo de nuevo.",
|
||||
"max": "max: {n}",
|
||||
"empty_content": "la nota está vacía."
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
"new_note": "nouvelle note",
|
||||
"new_note_notice": "<b>disponibilité :</b><br />il n'est pas garanti que la note reste stockée car tout est conservé dans la mémoire vive; si elle se remplit, les notes les plus anciennes seront supprimées.<br />(tout ira probablement bien, soyez juste averti.)",
|
||||
"errors": {
|
||||
"note_to_big": "Impossible de créer une note. La note est trop grande.",
|
||||
"note_too_big": "Impossible de créer une note. La note est trop grande.",
|
||||
"note_error": "n'a pas pu créer de note. Veuillez réessayer.",
|
||||
"max": "max: {n}",
|
||||
"empty_content": "La note est vide."
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
"new_note": "nuova nota",
|
||||
"new_note_notice": "<b>disponibilità:</b><br />la nota non è garantita per essere memorizzata come tutto è tenuto in ram, se si riempie le note più vecchie saranno rimosse.<br />(probabilmente andrà bene, basta essere avvertiti).",
|
||||
"errors": {
|
||||
"note_to_big": "impossibile creare una nota. la nota è troppo grande",
|
||||
"note_too_big": "impossibile creare una nota. la nota è troppo grande",
|
||||
"note_error": "Impossibile creare la nota. Riprova.",
|
||||
"max": "max: {n}",
|
||||
"empty_content": "la nota è vuota."
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
"new_note": "新しいメモ",
|
||||
"new_note_notice": "<b>可用性: </b> <br />すべてが RAM に保持されるため、メモが保存されるとは限りません。いっぱいになると、最も古いメモが削除されます。 <br /> (大丈夫だと思いますが、ご了承ください。)",
|
||||
"errors": {
|
||||
"note_to_big": "メモを作成できませんでした。メモが大きすぎる",
|
||||
"note_too_big": "メモを作成できませんでした。メモが大きすぎる",
|
||||
"note_error": "メモを作成できませんでした。もう一度お試しください。",
|
||||
"max": "最大ファイルサイズ: {n}",
|
||||
"empty_content": "メモは空です。"
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
"new_note": "nowa notatka",
|
||||
"new_note_notice": "<b>dostępność:</b><br />nie ma gwarancji, że notatka będzie przechowywana, ponieważ wszystko jest przechowywane w pamięci RAM, jeśli się zapełni, najstarsze notatki zostaną usunięte.<br />(prawdopodobnie nic się nie stanie, ale warto ostrzec.)",
|
||||
"errors": {
|
||||
"note_to_big": "nie można utworzyć notatki. notatka jest za duża",
|
||||
"note_too_big": "nie można utworzyć notatki. notatka jest za duża",
|
||||
"note_error": "nie można utworzyć notatki. spróbuj ponownie.",
|
||||
"max": "maks.: {n}",
|
||||
"empty_content": "notatka jest pusta."
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
"new_note": "новая заметка",
|
||||
"new_note_notice": "<b>доступность:</b><br />сохранение заметки не гарантируется, поскольку все хранится в оперативной памяти; если она заполнится, самые старые заметки будут удалены.<br />( вероятно, все будет в порядке, просто будьте осторожны.)",
|
||||
"errors": {
|
||||
"note_to_big": "нельзя создать новую заметку. заметка слишком большая",
|
||||
"note_too_big": "нельзя создать новую заметку. заметка слишком большая",
|
||||
"note_error": "нельзя создать новую заметку. пожалуйста попробуйте позже.",
|
||||
"max": "макс: {n}",
|
||||
"empty_content": "пустая заметка."
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
"new_note": "新筆記",
|
||||
"new_note_notice": "<b>可用性:</b><br />筆記不保證被儲存,因為所有內容都保留在 RAM 中,如果 RAM 填滿,最舊的筆記將被移除。<br />(您可能會沒事,只是提醒一下。)",
|
||||
"errors": {
|
||||
"note_to_big": "無法創建筆記。筆記過大",
|
||||
"note_too_big": "無法創建筆記。筆記過大",
|
||||
"note_error": "無法創建筆記。請再試一次。",
|
||||
"max": "最大值:{n}",
|
||||
"empty_content": "筆記內容為空。"
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
"new_note": "新建密信",
|
||||
"new_note_notice": "<b>提醒:</b><br>密信保存在内存中,如果内存满了,则最早的密信将被删除以释放内存,因此不保证该密信的可用性。一般不会出现这种情况,无需担心。",
|
||||
"errors": {
|
||||
"note_to_big": "创建失败,密信过大。",
|
||||
"note_too_big": "创建失败,密信过大。",
|
||||
"note_error": "创建失败,请稍后重试。",
|
||||
"max": "次数上限:{n}",
|
||||
"empty_content": "密信不能为空。"
|
||||
|
||||
@@ -60,7 +60,6 @@
|
||||
})
|
||||
|
||||
async function handlePaste(e: ClipboardEvent) {
|
||||
e.preventDefault()
|
||||
const data = e.clipboardData
|
||||
if (!data) return
|
||||
|
||||
@@ -79,6 +78,7 @@
|
||||
}
|
||||
|
||||
if (raw.length === 0) return
|
||||
e.preventDefault()
|
||||
|
||||
const seen = new Set<string>()
|
||||
const pasted: File[] = []
|
||||
@@ -149,7 +149,7 @@
|
||||
notify.success($t('home.messages.note_created'))
|
||||
} catch (e) {
|
||||
if (e instanceof PayloadToLargeError) {
|
||||
notify.error($t('home.errors.note_to_big'))
|
||||
notify.error($t('home.errors.note_too_big'))
|
||||
} else if (e instanceof EmptyContentError) {
|
||||
notify.error($t('home.errors.empty_content'))
|
||||
} else {
|
||||
|
||||
|
After Width: | Height: | Size: 5.6 KiB |
|
After Width: | Height: | Size: 5.6 KiB |
|
After Width: | Height: | Size: 5.6 KiB |
|
After Width: | Height: | Size: 5.6 KiB |
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"name": "@cryptgeon/shared",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": "./src/index.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@msgpack/msgpack": "^3.1.3",
|
||||
"@noble/ciphers": "^2.2.0",
|
||||
"@noble/hashes": "^2.2.0",
|
||||
"ky": "^2.0.2",
|
||||
"zod": "^4.4.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tsconfig/strictest": "catalog:",
|
||||
"@vitest/browser-playwright": "catalog:",
|
||||
"typescript": "catalog:",
|
||||
"vitest": "catalog:"
|
||||
},
|
||||
"scripts": {
|
||||
"test:browser": "vitest"
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 5.6 KiB |
|
After Width: | Height: | Size: 5.6 KiB |
|
After Width: | Height: | Size: 5.6 KiB |
|
After Width: | Height: | Size: 5.6 KiB |
@@ -0,0 +1,38 @@
|
||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`encryption > stable derive key 1`] = `
|
||||
Uint8Array [
|
||||
107,
|
||||
115,
|
||||
150,
|
||||
59,
|
||||
243,
|
||||
162,
|
||||
229,
|
||||
9,
|
||||
221,
|
||||
235,
|
||||
124,
|
||||
184,
|
||||
124,
|
||||
51,
|
||||
96,
|
||||
32,
|
||||
183,
|
||||
240,
|
||||
114,
|
||||
43,
|
||||
221,
|
||||
208,
|
||||
248,
|
||||
142,
|
||||
16,
|
||||
45,
|
||||
163,
|
||||
137,
|
||||
102,
|
||||
240,
|
||||
245,
|
||||
198,
|
||||
]
|
||||
`;
|
||||
@@ -0,0 +1,14 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { upload } from "./actions";
|
||||
import { deriveKey } from "./encryption";
|
||||
|
||||
describe("actions", () => {
|
||||
describe("upload", () => {
|
||||
it("upload", async () => {
|
||||
const key = deriveKey("abc");
|
||||
await expect(
|
||||
upload({ public: "foo", metadata: { views: 1 }, files: [] }, key),
|
||||
).resolves.toBe({});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,39 @@
|
||||
import { encode } from "@msgpack/msgpack";
|
||||
import { encrypt } from "./encryption";
|
||||
import { ClientNote, ServerNote } from "./types";
|
||||
import ky from "ky";
|
||||
import z from "zod";
|
||||
|
||||
const client = ky.extend({ baseUrl: "http://localhost:8000/api" });
|
||||
|
||||
export async function upload(
|
||||
note: ClientNote,
|
||||
key: Uint8Array,
|
||||
): Promise<string> {
|
||||
const data = encode(note.files);
|
||||
const encrypted = encrypt(data, key);
|
||||
const serverNote: ServerNote = {
|
||||
metadata: structuredClone(note.metadata),
|
||||
public: "",
|
||||
data: encrypted,
|
||||
};
|
||||
const payload = encode(serverNote);
|
||||
const response = await client
|
||||
.post("/notes/v2", {
|
||||
headers: {
|
||||
"content-type": "application/msgpack",
|
||||
},
|
||||
body: new Blob([payload]).stream(),
|
||||
})
|
||||
.json(
|
||||
z.object({
|
||||
id: z.string().nonempty(),
|
||||
}),
|
||||
);
|
||||
console.info("created note", response);
|
||||
throw new Error("not implemented");
|
||||
}
|
||||
|
||||
export function view(noteId: string): Promise<ClientNote> {
|
||||
throw new Error("not implemented");
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
decrypt,
|
||||
deriveKey,
|
||||
encrypt,
|
||||
generateKey,
|
||||
utf8ToBytes,
|
||||
} from "./encryption";
|
||||
|
||||
describe("encryption", () => {
|
||||
it("chacha20 custom key", () => {
|
||||
const password = "abc";
|
||||
const payload = utf8ToBytes("libero iste qui");
|
||||
|
||||
const key = deriveKey(password);
|
||||
const encrypted = encrypt(payload, key);
|
||||
const decrypted = decrypt(encrypted, key);
|
||||
expect(decrypted).toEqual(payload);
|
||||
});
|
||||
|
||||
it("chacha20 auto key", () => {
|
||||
const payload = utf8ToBytes(
|
||||
"Earum id inventore debitis rerum minima necessitatibus consequuntur.",
|
||||
);
|
||||
const key = generateKey();
|
||||
const encrypted = encrypt(payload, key);
|
||||
const decrypted = decrypt(encrypted, key);
|
||||
expect(decrypted).toEqual(payload);
|
||||
});
|
||||
|
||||
it("stable derive key", () => {
|
||||
const password = "alias laboriosam porro";
|
||||
expect(deriveKey(password)).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("derived key has same length as generated", () => {
|
||||
const derived = deriveKey("ipsam esse asperiores");
|
||||
const generated = generateKey();
|
||||
|
||||
expect(derived.length).toEqual(generated.length);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,40 @@
|
||||
import { xchacha20poly1305 } from "@noble/ciphers/chacha.js";
|
||||
import {
|
||||
managedNonce,
|
||||
randomBytes,
|
||||
utf8ToBytes,
|
||||
} from "@noble/ciphers/utils.js";
|
||||
import { scrypt } from "@noble/hashes/scrypt.js";
|
||||
|
||||
export { bytesToUtf8, utf8ToBytes } from "@noble/ciphers/utils.js";
|
||||
|
||||
const APP_SPECIFIC_SECRET = "758ac0b9d5f04efca13f57909d3d0fc0";
|
||||
const SECURITY_LEVEL = 2 ** 15;
|
||||
const KEY_SIZE = 32;
|
||||
|
||||
export function deriveKey(password: string) {
|
||||
const key = scrypt(password, APP_SPECIFIC_SECRET, {
|
||||
N: SECURITY_LEVEL,
|
||||
r: 8,
|
||||
p: 1,
|
||||
dkLen: KEY_SIZE,
|
||||
});
|
||||
return key;
|
||||
}
|
||||
|
||||
export function generateKey(): Uint8Array {
|
||||
return randomBytes(KEY_SIZE);
|
||||
}
|
||||
|
||||
export function encrypt(payload: Uint8Array, key: Uint8Array): Uint8Array {
|
||||
const chacha = managedNonce(xchacha20poly1305)(key);
|
||||
const data = payload instanceof Uint8Array ? payload : utf8ToBytes(payload);
|
||||
const ciphertext = chacha.encrypt(data);
|
||||
return ciphertext;
|
||||
}
|
||||
|
||||
export function decrypt(ciphertext: Uint8Array, key: Uint8Array): Uint8Array {
|
||||
const chacha = managedNonce(xchacha20poly1305)(key);
|
||||
const decrypted = chacha.decrypt(ciphertext, key);
|
||||
return decrypted;
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * as encryption from "./encryption";
|
||||
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* `metadata` is public, and NOT encrypted
|
||||
*/
|
||||
export type NoteMetadata = {
|
||||
expiration?: number;
|
||||
views?: number;
|
||||
};
|
||||
|
||||
export type ClientNote = {
|
||||
metadata: NoteMetadata;
|
||||
|
||||
/**
|
||||
* public information
|
||||
*/
|
||||
public: string;
|
||||
|
||||
/**
|
||||
* `files` are encrypted
|
||||
*/
|
||||
files: {
|
||||
name: string;
|
||||
type: string;
|
||||
content: Uint8Array;
|
||||
}[];
|
||||
};
|
||||
|
||||
export type ServerNote = {
|
||||
metadata: NoteMetadata;
|
||||
|
||||
/**
|
||||
* Publicly available information
|
||||
*/
|
||||
public: string;
|
||||
|
||||
/**
|
||||
* Encrypted binary blob
|
||||
*/
|
||||
data: Uint8Array;
|
||||
};
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": ["@tsconfig/strictest"],
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import { defineConfig } from "vitest/config";
|
||||
import { playwright } from "@vitest/browser-playwright";
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
projects: [
|
||||
{ test: { name: "node", environment: "node" } },
|
||||
{
|
||||
test: {
|
||||
name: "Browser",
|
||||
browser: {
|
||||
enabled: true,
|
||||
provider: playwright(),
|
||||
headless: true,
|
||||
// https://vitest.dev/config/browser/playwright
|
||||
instances: [
|
||||
{ browser: "chromium" },
|
||||
{ browser: "firefox" },
|
||||
{ browser: "webkit" },
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
@@ -5,3 +5,9 @@ allowBuilds:
|
||||
esbuild: true
|
||||
|
||||
minimumReleaseAge: 10080 # One week
|
||||
|
||||
catalog:
|
||||
"@tsconfig/strictest": "^2.0.8"
|
||||
"typescript": "^6.0.3"
|
||||
"vitest": "^4.1.7"
|
||||
"@vitest/browser-playwright": "^4.1.7"
|
||||
|
||||