Skip to content

Commit

Permalink
init: functional 1.0
Browse files Browse the repository at this point in the history
  • Loading branch information
hUwUtao committed Jul 21, 2023
0 parents commit 61e05ce
Show file tree
Hide file tree
Showing 15 changed files with 404 additions and 0 deletions.
6 changes: 6 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
root = true

[*]
end_of_line = lf
indent_style = space
indent_size = 2
2 changes: 2 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
DATABASE_URL="mysql://user:password@localhost:3306/database_name"
PRIVATE_KEY=aNywgbliberhHEre.AccEPtINRangeUTf8
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
node_modules
# Keep environment variables out of version control
.env
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# `nut`

An authentication server.

## How to use (standalone)

- Copy `.env.example` to `.env` and fill it.
- `bun start`
- Reverse proxy the port, specified by `PORT` enviroment variable. `8080` by default

## How to use (embedded)

I should involve, no worries.
Binary file added bun.lockb
Binary file not shown.
146 changes: 146 additions & 0 deletions main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
import { PrismaClient } from "@prisma/client";
import { ErrorMessage } from "./types/shared";
import { saltcheck } from "./utils/cipher";
import { protocol, tokener, verify } from "./trusted";

const prisma = new PrismaClient();

const mgex = /^[a-zA-Z0-9_]{2,16}$/;

type FutureResponse = Promise<Response>;
type JSONAble = object | boolean | number | string | null | undefined;

class JSONResponse extends Response {
constructor(body: JSONAble, options?: ResponseInit) {
super(JSON.stringify(body), {
headers: {
"Content-Type": "application/json",
},
...options,
});
}
}

class ErrorResponse extends JSONResponse {
constructor(body: ErrorMessage, status: number, options?: ResponseInit) {
super(body, {
status,
...options,
});
}
}

module api {
/* Auth */
/**
* #### This route provide a user-token for user-related service
* > Route accept `urlencoded form` body. `POST` only
*
* `username` is a minecraft username, lowercased
*
* `password` is user's password hashed in sha256
*/
export async function auth(req: Request): FutureResponse {
const fd = await req.formData();

if (!fd.has("username") || !fd.has("password"))
return new ErrorResponse(
{
success: false,
msg: "Missing body parameters",
},
400
);
const username = (fd.get("username") as string).toLowerCase();
if (!mgex.test(username))
return new ErrorResponse(
{
success: false,
msg: "Wrong username format?",
},
400
);
const fetch = await prisma.credential.findUnique({
where: {
username: username,
},
select: {
password: true,
realname: true,
},
});
if (fetch) {
const { password, realname } = fetch;
if (saltcheck(fd.get("password") as string, password)) {
const f = protocol.serialize(username);
return new JSONResponse({
success: true,
msg: "Authenticated as " + realname,
token: tokener.sign(f),
});
} else
return new ErrorResponse(
{ success: false, msg: "Password check failed!", code: "fpwd" },
401
);
} else {
return new ErrorResponse(
{ success: false, msg: "User not found?", code: "nusr" },
404
);
}
}

export async function auth_verify(req: Request) {
const to = req.headers.get("Authorization")?.split(" "),
ve = to && to[0] == "Bearer" && verify(to[1]);
return ve
? new JSONResponse({
success: true,
msg: "Authenticated as " + ve.username,
data: ve,
})
: new ErrorResponse(
{
success: false,
msg: "Token invalid",
},
401
);
}
}

/* HTTP Flow */

class Resolver {
readonly url: URL;
readonly req: Request;
constructor(req: Request) {
this.req = req;
this.url = new URL(req.url);
}
match(matcher: string) {
const pathname = this.url.pathname;
return pathname == matcher;
}
matchMethod(method: string, matcher: string) {
return this.match(matcher) && this.req.method === method;
}
}

Bun.serve({
port: 8080,
fetch(req: Request) {
return new Promise<Response>((ok, panic) => {
const rs = new Resolver(req);
if (rs.matchMethod("POST", "/api/auth"))
api.auth(req).then(ok).catch(panic);
else if (rs.match("/api/auth/verify"))
api.auth_verify(req).then(ok).catch(panic);
else if (rs.match("/")) ok(new Response("Home"));
else ok(new Response("no?", { status: 404 }));
});
},
});

export {};
24 changes: 24 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"name": "nut",
"proto": 1,
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"@types/node": "^20.4.2",
"bun-types": "^0.6.14",
"prisma": "^5.0.0",
"ts-node": "^10.9.1",
"typescript": "^5.1.6"
},
"dependencies": {
"@prisma/client": "5.0.0",
"serdebin": "^1.1.0"
}
}
50 changes: 50 additions & 0 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema

generator client {
provider = "prisma-client-js"
}

datasource db {
provider = "mysql"
url = env("DATABASE_URL")
}

// Custom

model Profile {
id Int @id @default(autoincrement()) @db.UnsignedInt
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Profile info
// Rel
credential Credential @relation(fields: [cid], references: [id])
cid Int @unique @db.UnsignedInt
}

model Credential {
id Int @id @default(autoincrement()) @db.UnsignedInt
username String @unique @db.Char(16)
realname String @db.Char(16)
password String @default("no")
ip String?
lastlogin BigInt? @db.UnsignedBigInt
w String @default("l") @db.Char(16)
x Float @default(0) @db.Double
y Float @default(0) @db.Double
z Float @default(0) @db.Double
v Float @default(0) @db.Double
h Float @default(0) @db.Double
regdate BigInt @default(0) @db.UnsignedBigInt
regip String? @default("::1") @db.Char(39)
email String? @db.VarChar(255)
isLogged Boolean @default(false)
hasSession Boolean @default(false)
totp String? @db.VarChar(32)
uuid String? @db.Char(32)
Profile Profile?
@@map(name: "am")
}
70 changes: 70 additions & 0 deletions trusted.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/**
* This interface allow allies in swarm who share the same private key
* in order to verify enemy hash
*/

import { build, u } from "serdebin/helper";
import { Deserialization, extract_str, makeFixed } from "serdebin";
import pkgjson from "./package.json";
import Engine from "./utils/token";
import { trimStartPad } from "./utils/bytewise";

const pk = process.env.PRIVATE_KEY,
proto = pkgjson.proto;

if (!pk)
throw "Please run with PRIVATE_KEY enviroment variable, or add it into .env file";
const tokener = new Engine(pk);

/**
* To keep long number remain compact, binary is used.
*
* Unfortunaltely, you have to copy the size from serialize function to deserialize function.
* There is no automation
*
* Example, current time in UNIX is 13 character, but under number bit level, it only cost 5 and 1/4 byte.
*/
export module protocol {
/**
* swarmer have anility to grant the token.
*
* however the main job upon the OAuth server
* @param username player's username
*/
export function serialize(username: string) {
return build(
// bit-pad ofuscator
false,
// protocol version
u(7, proto),
// username
makeFixed(extract_str(username), 16 * 8),
// validUntil
u(42, Date.now())
);
}
/**
* once data is trustable, deserialize it
* @param reader Data reader
*/
export function deserialize(reader: Deserialization) {
reader.b(); // left pad
return {
proto: reader.u(7),
username: trimStartPad(reader.s(16)),
expire: reader.u(42),
};
}
}

/**
* This quantum function may share across trusted NodeJS server
* @param hash hash stored in header
* @returns payloads
*/
export function verify(hash: string) {
const reader = tokener.verify(hash);
return reader && protocol.deserialize(reader);
}

export { tokener };
25 changes: 25 additions & 0 deletions tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"compilerOptions": {
// add Bun type definitions
"types": ["bun-types"],

// enable latest features
"lib": ["esnext"],
"module": "esnext",
"target": "esnext",

// if TS 5.x+
"moduleResolution": "bundler",
// "allowImportingTsExtensions": true,
"moduleDetection": "force",

"jsx": "react-jsx", // support JSX
"allowJs": true, // allow importing `.js` from `.ts`
"esModuleInterop": true, // allow default imports for CommonJS modules

// best practices
"strict": true,
"forceConsistentCasingInFileNames": true,
"skipLibCheck": true
}
}
8 changes: 8 additions & 0 deletions types/shared.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export type Message = {
success: true;
};
export type ErrorMessage = {
success: false;
msg: string;
code?: string;
};
16 changes: 16 additions & 0 deletions utils/bytewise.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
export function joinu8a(...arrays: Uint8Array[]) {
let length = arrays.reduce((acc, arr) => acc + arr.length, 0);
let mergedArray = new Uint8Array(length);
let offset = 0;
arrays.forEach((arr) => {
mergedArray.set(arr, offset);
offset += arr.length;
});
return mergedArray.buffer;
}

export function trimStartPad(d: string) {
var i = 0;
while (d[i] == "\u0000" && i < d.length) i++;
return d.slice(i);
}
8 changes: 8 additions & 0 deletions utils/cipher.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { sha256 } from "./digest";

export function saltcheck(phPwd: string, hashd: string): boolean {
// $SHA$salt$hash, where hash := sha256(sha256(password) . salt)
// somehow this thing might work
const [_, __, salt, salten] = hashd.split("$");
return sha256(phPwd + salt) === salten;
}
6 changes: 6 additions & 0 deletions utils/digest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export function sha256(i: string | Uint8Array | ArrayBuffer): string {
// return crypto.createHash('sha256').update(str).digest('hex');
const hashr = new Bun.CryptoHasher("sha256")
hashr.update(i)
return hashr.digest("hex");
}
Loading

0 comments on commit 61e05ce

Please sign in to comment.