msgpack
@@ -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();
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
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" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||