Skip to content

Commit

Permalink
Document revamp & server-side encryption (#86)
Browse files Browse the repository at this point in the history
Co-authored-by: tnfAngel <[email protected]>
  • Loading branch information
inetol and tnfAngel authored Apr 14, 2024
1 parent cdfb89b commit 7cc4d90
Show file tree
Hide file tree
Showing 16 changed files with 214 additions and 155 deletions.
3 changes: 1 addition & 2 deletions biome.json
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
{
"$schema": "https://biomejs.dev/schemas/1.6.4/schema.json",
"files": {
"ignore": ["**/node_modules/", "documents/", "dist/"],
"ignoreUnknown": true
},
"formatter": {
"enabled": true,
"formatWithErrors": false,
"ignore": ["**/node_modules/", "documents/", "src/structures/", "dist/"],
"indentStyle": "tab",
"indentWidth": 4,
"lineEnding": "lf",
Expand All @@ -32,7 +32,6 @@
},
"linter": {
"enabled": true,
"ignore": ["**/node_modules/", "documents/", "src/structures/", "dist/"],
"rules": {
"recommended": true,
"style": {
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
"lint:biome": "bun --bun biome lint .",
"lint:tsc": "bun --bun tsc --noEmit",
"start": "bun run build && bun --bun ./dist/backend.js",
"start:dev": "bun --bun --watch ./src/index.ts"
"start:dev": "bun --bun ./src/index.ts"
},
"dependencies": {
"@elysiajs/swagger": "~1.0.3",
Expand Down
155 changes: 87 additions & 68 deletions src/classes/DocumentHandler.ts
Original file line number Diff line number Diff line change
@@ -1,124 +1,138 @@
import { unlink } from 'node:fs/promises';
import type { BunFile } from 'bun';
import { decode, encode } from 'cbor-x';
import type { CompatDocumentStruct, Parameters } from '../types/DocumentHandler.ts';
import type { DocumentV1, Parameters, ResponsesV1, ResponsesV2 } from '../types/DocumentHandler.ts';
import { ErrorCode } from '../types/ErrorHandler.ts';
import { ServerEndpointVersion } from '../types/Server.ts';
import { CryptoUtils } from '../utils/CryptoUtils.ts';
import { StringUtils } from '../utils/StringUtils.ts';
import { ValidatorUtils } from '../utils/ValidatorUtils.ts';
import { ErrorHandler } from './ErrorHandler.ts';
import { Server } from './Server.ts';

export class DocumentHandler {
public static async compatDocumentRead(file: BunFile): Promise<CompatDocumentStruct> {
return decode(Bun.inflateSync(await file.arrayBuffer()));
}

public static async compatDocumentWrite(filePath: string, document: CompatDocumentStruct): Promise<void> {
await Bun.write(filePath, Bun.deflateSync(encode(document)));
}
type ResponseByVersion<V extends ServerEndpointVersion> = V extends ServerEndpointVersion.V1
? ResponsesV1
: ResponsesV2;

export class DocumentHandler {
public static async accessRaw(params: Parameters['access']) {
DocumentHandler.validateKey(params.key);

const file = await DocumentHandler.retrieveDocument(params.key);
const document = await DocumentHandler.compatDocumentRead(file);
const document = await DocumentHandler.documentRead(file);
let data = document.data;

if (document.header.dataHash) {
DocumentHandler.validatePassword(params.password, document.header.dataHash);

if (params.password) {
data = CryptoUtils.decrypt(document.data, params.password);
}
}

DocumentHandler.validateTimestamp(params.key, document.expirationTimestamp);
await DocumentHandler.validatePassword(params.password, document.password);
data = Bun.inflateSync(data);

return new Response(document.rawFileData);
return new TextDecoder().decode(data);
}

public static async access(params: Parameters['access'], version: ServerEndpointVersion) {
public static async access<EndpointVersion extends ServerEndpointVersion>(
params: Parameters['access'],
version: EndpointVersion
): Promise<ResponseByVersion<EndpointVersion>['access']> {
DocumentHandler.validateKey(params.key);

const file = await DocumentHandler.retrieveDocument(params.key);
const document = await DocumentHandler.compatDocumentRead(file);
const document = await DocumentHandler.documentRead(file);
let data = document.data;

if (document.header.dataHash) {
DocumentHandler.validatePassword(params.password, document.header.dataHash);

DocumentHandler.validateTimestamp(params.key, document.expirationTimestamp);
await DocumentHandler.validatePassword(params.password, document.password);
if (params.password) {
data = CryptoUtils.decrypt(document.data, params.password);
}
}

const data = new TextDecoder().decode(document.rawFileData);
data = Bun.inflateSync(data);

switch (version) {
case ServerEndpointVersion.V1: {
return { key: params.key, data };
return { key: params.key, data: new TextDecoder().decode(data) };
}

case ServerEndpointVersion.V2: {
return {
key: params.key,
data,
data: new TextDecoder().decode(data),
url: Server.HOSTNAME.concat('/', params.key),
expirationTimestamp: document.expirationTimestamp ?? -1
// Deprecated, for compatibility reasons will be kept to 0
expirationTimestamp: 0
};
}

default: {
throw new Error(`Unsupported version: ${version}`);
}
}
}

public static async edit(params: Parameters['edit']) {
DocumentHandler.validateKey(params.key);

const file = await DocumentHandler.retrieveDocument(params.key);
const document = await DocumentHandler.compatDocumentRead(file);
const document = await DocumentHandler.documentRead(file);

DocumentHandler.validateSecret(params.secret, document.secret);
DocumentHandler.validateSecret(params.secret, document.header.modHash);
DocumentHandler.validateSizeBetweenLimits(params.body);

const buffer = Buffer.from(params.body as ArrayBuffer);
const bodyPack = Bun.deflateSync(params.body);

DocumentHandler.validateSizeBetweenLimits(buffer);

document.rawFileData = buffer;
document.data = params.password ? CryptoUtils.encrypt(bodyPack, params.password) : bodyPack;

return {
edited: await DocumentHandler.compatDocumentWrite(Server.DOCUMENT_PATH + params.key, document)
edited: await DocumentHandler.documentWrite(Server.DOCUMENT_PATH + params.key, document)
.then(() => true)
.catch(() => false)
};
}

public static async exists(params: Parameters['exists']) {
public static exists(params: Parameters['exists']) {
DocumentHandler.validateKey(params.key);

return Bun.file(Server.DOCUMENT_PATH + params.key).exists();
}

public static async publish(params: Parameters['publish'], version: ServerEndpointVersion) {
public static async publish<EndpointVersion extends ServerEndpointVersion>(
params: Parameters['publish'],
version: EndpointVersion
): Promise<ResponseByVersion<EndpointVersion>['publish']> {
DocumentHandler.validateSelectedKey(params.selectedKey);
DocumentHandler.validateSelectedKeyLength(params.selectedKeyLength);
DocumentHandler.validatePasswordLength(params.password);

const secret = params.selectedSecret || StringUtils.createSecret();

DocumentHandler.validateSecretLength(secret);
DocumentHandler.validateSizeBetweenLimits(params.body);

const bodyArray = new Uint8Array(params.body);

DocumentHandler.validateSizeBetweenLimits(bodyArray);

let lifetime = params.lifetime ?? Server.DOCUMENT_MAXTIME;

// Make the document permanent if the value exceeds 5 years
if (lifetime > 157_784_760) lifetime = 0;

const msLifetime = lifetime * 1000;
const expirationTimestamp = msLifetime > 0 ? Date.now() + msLifetime : 0;

const bodyPack = Bun.deflateSync(params.body);
const key = params.selectedKey || (await StringUtils.createKey(params.selectedKeyLength));

if (params.selectedKey && (await StringUtils.keyExists(key))) {
ErrorHandler.send(ErrorCode.documentKeyAlreadyExists);
}

const document: CompatDocumentStruct = {
rawFileData: bodyArray,
secret,
expirationTimestamp,
password: params.password ? await Bun.password.hash(params.password) : null
const document: DocumentV1 = {
data: params.password ? CryptoUtils.encrypt(bodyPack, params.password) : bodyPack,
header: {
dataHash: params.password ? CryptoUtils.hash(params.password) : null,
modHash: CryptoUtils.hash(secret),
createdAt: Date.now()
},
version: 1
};

await DocumentHandler.compatDocumentWrite(Server.DOCUMENT_PATH + key, document);
await DocumentHandler.documentWrite(Server.DOCUMENT_PATH + key, document);

switch (version) {
case ServerEndpointVersion.V1: {
Expand All @@ -129,20 +143,25 @@ export class DocumentHandler {
return {
key,
secret,
url: Server.HOSTNAME.concat('/', key) + (params.password ? `/?p=${params.password}` : ''),
expirationTimestamp: expirationTimestamp
url: Server.HOSTNAME.concat('/', key),
// Deprecated, for compatibility reasons will be kept to 0
expirationTimestamp: 0
};
}

default: {
throw new Error(`Unsupported version: ${version}`);
}
}
}

public static async remove(params: Parameters['remove']) {
DocumentHandler.validateKey(params.key);

const file = await DocumentHandler.retrieveDocument(params.key);
const document = await DocumentHandler.compatDocumentRead(file);
const document = await DocumentHandler.documentRead(file);

DocumentHandler.validateSecret(params.secret, document.secret);
DocumentHandler.validateSecret(params.secret, document.header.modHash);

return {
removed: await unlink(Server.DOCUMENT_PATH + params.key)
Expand All @@ -151,6 +170,14 @@ export class DocumentHandler {
};
}

private static async documentRead(file: BunFile): Promise<DocumentV1> {
return decode(new Uint8Array(await file.arrayBuffer()));
}

private static async documentWrite(filePath: string, document: DocumentV1): Promise<void> {
await Bun.write(filePath, encode(document));
}

private static async retrieveDocument(key: string): Promise<BunFile> {
const file = Bun.file(Server.DOCUMENT_PATH + key);

Expand All @@ -174,8 +201,8 @@ export class DocumentHandler {
}
}

private static validateSecret(secret: string, documentSecret: CompatDocumentStruct['secret']): void {
if (documentSecret && documentSecret !== secret) {
private static validateSecret(secret: string, documentSecret: DocumentV1['header']['modHash']): void {
if (!CryptoUtils.compare(secret, documentSecret)) {
ErrorHandler.send(ErrorCode.documentInvalidSecret);
}
}
Expand All @@ -189,12 +216,12 @@ export class DocumentHandler {
}
}

private static async validatePassword(
private static validatePassword(
password: string | undefined,
documentPassword: CompatDocumentStruct['password']
): Promise<void> {
documentPassword: DocumentV1['header']['dataHash']
): void {
if (documentPassword) {
if (!password || !(await Bun.password.verify(password, documentPassword))) {
if (password && !CryptoUtils.compare(password, documentPassword)) {
ErrorHandler.send(ErrorCode.documentInvalidPassword);
}
}
Expand All @@ -210,16 +237,8 @@ export class DocumentHandler {
}
}

private static validateTimestamp(key: string, timestamp: CompatDocumentStruct['expirationTimestamp']): void {
if (timestamp && ValidatorUtils.isLengthWithinRange(timestamp, 1, Date.now())) {
unlink(Server.DOCUMENT_PATH + key);

ErrorHandler.send(ErrorCode.documentNotFound);
}
}

private static validateSizeBetweenLimits(body: Uint8Array): void {
if (!ValidatorUtils.isLengthWithinRange(body.length, 1, Server.DOCUMENT_MAXLENGTH)) {
private static validateSizeBetweenLimits(body: any): void {
if (!ValidatorUtils.isLengthWithinRange(Buffer.byteLength(body, 'utf8'), 1, Server.DOCUMENT_MAXLENGTH)) {
ErrorHandler.send(ErrorCode.documentInvalidLength);
}
}
Expand Down
1 change: 1 addition & 0 deletions src/classes/Server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ export class Server {
}

default: {
// FIXME: Returns some errors without following error scheme
return error;
}
}
Expand Down
8 changes: 4 additions & 4 deletions src/endpoints/AccessRawV1.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import { ErrorHandler } from '../classes/ErrorHandler.ts';

export class AccessRawV1 extends AbstractEndpoint {
protected override run(): void {
// FIXME: Implicit "any" type on "set" & "params", careful...
this.SERVER.elysia.get(
this.PREFIX.concat('/:key/raw'),
async ({ set, params }) => {
Expand All @@ -27,16 +26,17 @@ export class AccessRawV1 extends AbstractEndpoint {
}
),
response: {
200: t.Any({
200: t.String({
description: 'The raw document',
examples: ['Hello world']
examples: ['Hello, World!']
}),
400: ErrorHandler.SCHEMA,
404: ErrorHandler.SCHEMA
},
detail: {
summary: 'Get raw document',
tags: ['v1']
tags: ['v1'],
deprecated: true
}
}
);
Expand Down
Loading

0 comments on commit 7cc4d90

Please sign in to comment.