This commit is contained in:
2026-06-07 10:46:15 +02:00
parent bb422fdd8d
commit 781231e414
21 changed files with 443 additions and 0 deletions
+5
View File
@@ -0,0 +1,5 @@
mod model;
mod routes;
pub use model::*;
pub use routes::*;
+41
View File
@@ -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;
}
+126
View File
@@ -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();
// }
// }
// }
Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

+24
View File
@@ -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"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

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,
]
`;
+14
View File
@@ -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({});
});
});
});
+39
View File
@@ -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");
}
+42
View File
@@ -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);
});
});
+40
View File
@@ -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;
}
+1
View File
@@ -0,0 +1 @@
export * as encryption from "./encryption";
+39
View File
@@ -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;
};
+8
View File
@@ -0,0 +1,8 @@
{
"extends": ["@tsconfig/strictest"],
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "bundler",
},
}
+26
View File
@@ -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" },
],
},
},
},
],
},
});