mirror of
https://github.com/cupcakearmy/cryptgeon.git
synced 2026-09-26 20:41:45 +00:00
first v3
This commit is contained in:
@@ -0,0 +1,58 @@
|
||||
import { encode, decode } from "@msgpack/msgpack";
|
||||
|
||||
import type { ServerNote } from "./types.js";
|
||||
|
||||
let server = "";
|
||||
|
||||
export function setServer(url: string) {
|
||||
server = url.replace(/\/+$/, "");
|
||||
}
|
||||
|
||||
export function getServer() {
|
||||
return server;
|
||||
}
|
||||
|
||||
function api(path: string) {
|
||||
return `${server}/api/v3/${path}`;
|
||||
}
|
||||
|
||||
export async function create(note: ServerNote): Promise<{ id: string }> {
|
||||
const res = await fetch(api("notes"), {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/msgpack" },
|
||||
body: encode(note),
|
||||
});
|
||||
if (!res.ok) throw new Error("create failed");
|
||||
const buf = await res.arrayBuffer();
|
||||
const data = decode(new Uint8Array(buf)) as any;
|
||||
if (typeof data?.id !== "string") throw new Error("invalid response");
|
||||
return { id: data.id };
|
||||
}
|
||||
|
||||
export async function info(id: string): Promise<ServerNote["meta"] | null> {
|
||||
const res = await fetch(api(`notes/${id}`));
|
||||
if (!res.ok) return null;
|
||||
const buf = await res.arrayBuffer();
|
||||
const data = decode(new Uint8Array(buf)) as any;
|
||||
const meta = data?.meta as ServerNote["meta"] | undefined;
|
||||
if (!meta) return null;
|
||||
if (meta.extra && !(meta.extra instanceof Uint8Array)) meta.extra = new Uint8Array(meta.extra as any);
|
||||
return meta;
|
||||
}
|
||||
|
||||
export async function get(id: string): Promise<ServerNote | null> {
|
||||
const res = await fetch(api(`notes/${id}`), { method: "DELETE" });
|
||||
if (!res.ok) return null;
|
||||
const buf = await res.arrayBuffer();
|
||||
const data = decode(new Uint8Array(buf)) as any;
|
||||
const meta = data.meta as ServerNote["meta"];
|
||||
if (meta?.extra && !(meta.extra instanceof Uint8Array)) meta.extra = new Uint8Array(meta.extra as any);
|
||||
const d = data.data;
|
||||
return { meta, data: d instanceof Uint8Array ? d : new Uint8Array(d) } satisfies ServerNote;
|
||||
}
|
||||
|
||||
export async function status(): Promise<Record<string, unknown>> {
|
||||
const res = await fetch(api("status"));
|
||||
if (!res.ok) throw new Error("status failed");
|
||||
return res.json();
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { deriveKey, encrypt, decrypt, generateKey, utf8ToBytes, randomBytes } from "./crypto";
|
||||
|
||||
describe("crypto", () => {
|
||||
it("encrypts and decrypts with generated key", () => {
|
||||
const data = utf8ToBytes("hello world");
|
||||
const key = generateKey();
|
||||
const enc = encrypt(data, key);
|
||||
const dec = decrypt(enc, key);
|
||||
expect(dec).toEqual(data);
|
||||
});
|
||||
|
||||
it("encrypts and decrypts with derived key", () => {
|
||||
const data = utf8ToBytes("secret message");
|
||||
const salt = randomBytes(16);
|
||||
const key = deriveKey("password123", salt);
|
||||
const enc = encrypt(data, key);
|
||||
const dec = decrypt(enc, key);
|
||||
expect(dec).toEqual(data);
|
||||
});
|
||||
|
||||
it("derived key has same length as generated", () => {
|
||||
const salt = randomBytes(16);
|
||||
expect(deriveKey("test", salt).length).toBe(generateKey().length);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,41 @@
|
||||
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";
|
||||
export { randomBytes } from "@noble/ciphers/utils.js";
|
||||
|
||||
const N = 2 ** 15;
|
||||
const KEY_SIZE = 32;
|
||||
|
||||
export function generateKey(): Uint8Array {
|
||||
return randomBytes(KEY_SIZE);
|
||||
}
|
||||
|
||||
export function deriveKey(password: string, salt: Uint8Array): Uint8Array {
|
||||
return scrypt(password, salt, { N, r: 8, p: 1, dkLen: KEY_SIZE });
|
||||
}
|
||||
|
||||
export function encrypt(data: Uint8Array, key: Uint8Array): Uint8Array {
|
||||
const chacha = managedNonce(xchacha20poly1305)(key);
|
||||
return chacha.encrypt(data);
|
||||
}
|
||||
|
||||
export function decrypt(data: Uint8Array, key: Uint8Array): Uint8Array {
|
||||
const chacha = managedNonce(xchacha20poly1305)(key);
|
||||
return chacha.decrypt(data);
|
||||
}
|
||||
|
||||
export function bytesToHex(bytes: Uint8Array): string {
|
||||
return Array.from(bytes)
|
||||
.map((b) => b.toString(16).padStart(2, "0"))
|
||||
.join("");
|
||||
}
|
||||
|
||||
export function hexToBytes(hex: string): Uint8Array {
|
||||
const bytes = new Uint8Array(hex.length / 2);
|
||||
for (let i = 0; i < bytes.length; i++) {
|
||||
bytes[i] = Number.parseInt(hex.slice(i * 2, i * 2 + 2), 16);
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export * from "./crypto.js";
|
||||
export * from "./types.js";
|
||||
export * from "./api.js";
|
||||
export { encode, decode } from "@msgpack/msgpack";
|
||||
@@ -0,0 +1,21 @@
|
||||
export type NoteMeta = {
|
||||
expiration?: number;
|
||||
views?: number;
|
||||
extra?: Uint8Array;
|
||||
};
|
||||
|
||||
export type ServerNote = {
|
||||
meta: NoteMeta;
|
||||
data: Uint8Array;
|
||||
};
|
||||
|
||||
export type NoteContent =
|
||||
| { type: "text"; data: string }
|
||||
| { type: "files"; data: FileDTO[] };
|
||||
|
||||
export type FileDTO = {
|
||||
name: string;
|
||||
mime: string;
|
||||
size: number;
|
||||
data: Uint8Array;
|
||||
};
|
||||
Reference in New Issue
Block a user