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