diff --git a/apps/boko-framework/README.md b/apps/boko-framework/README.md new file mode 100644 index 0000000..d5f7856 --- /dev/null +++ b/apps/boko-framework/README.md @@ -0,0 +1,177 @@ +# Boko Framework + +**Async web framework for Bux — inspired by FastAPI.** + +Boko is a lightweight, multi-threaded web framework that brings FastAPI-style routing to the Bux programming language. It handles HTTP parsing, path pattern matching, query parameter extraction, and response building so you can focus on your application logic. + +## Quick Start + +```bash +cd apps/boko-framework +../../buxc build +./boko-framework +``` + +Open `http://localhost:8080` — you'll see the demo landing page. + +## How It Works + +Boko follows a **single-dispatch-function** pattern. You define one function — `Boko_Router` — and the framework calls it for every incoming request: + +```bux +import Boko::{Request, Response, Response_Json, Response_Html, Response_NotFound, + Path_Match, Request_GetQuery, Request_GetPathParam, + App, App_New, App_Run}; + +func Boko_Router(req: Request) -> Response { + // Route: GET / + if String_Eq(req.path, "/") { + return Response_Html("

Hello Boko!

"); + } + + // Route: GET /api/health + if String_Eq(req.path, "/api/health") { + return Response_Json("{\"status\":\"ok\"}"); + } + + // Route: GET /users/{id} — path parameter + if Path_Match("/users/{id}", req.path, &req) { + let id: String = Request_GetPathParam(&req, "id"); + // Build JSON response with id + return Response_Json(...); + } + + // Route: GET /search?q=... — query parameter + if String_Eq(req.path, "/search") { + let q: String = Request_GetQuery(&req, "q"); + return Response_Json(...); + } + + return Response_NotFound(); +} + +func Main() -> int { + let app: App = App_New(8080, 4); // port, threads + App_Run(&app); + return 0; +} +``` + +## API Reference + +### Request + +| Field | Type | Description | +|-------|------|-------------| +| `method` | `HttpVerb` | GET, POST, PUT, DELETE | +| `path` | `String` | Request path (without query string) | +| `body` | `String` | Request body (for POST/PUT) | +| `headerCount` | `int` | Number of headers | + +| Function | Returns | Description | +|----------|---------|-------------| +| `Request_GetHeader(req, name)` | `String` | Get header value by name | +| `Request_GetQuery(req, name)` | `String` | Get query parameter by name | +| `Request_GetPathParam(req, name)` | `String` | Get extracted path parameter | + +### Response + +| Constructor | Content-Type | Status | +|-------------|-------------|--------| +| `Response_Html(html)` | `text/html` | 200 | +| `Response_Json(json)` | `application/json` | 200 | +| `Response_Text(text)` | `text/plain` | 200 | +| `Response_Redirect(url)` | — | 302 | +| `Response_NotFound()` | `application/json` | 404 | +| `Response_Error(code, msg)` | `application/json` | custom | +| `Response_NoContent()` | — | 204 | + +### Path Matching + +`Path_Match(pattern, actualPath, req)` matches a pattern with `{param}` placeholders: + +```bux +// Pattern: /users/{id}/posts/{postId} +// Actual: /users/42/posts/7 +// Extracts: id=42, postId=7 + +if Path_Match("/users/{id}/posts/{postId}", req.path, &req) { + let userId: String = Request_GetPathParam(&req, "id"); // "42" + let postId: String = Request_GetPathParam(&req, "postId"); // "7" +} +``` + +The function returns `true` if the pattern matches and populates `req.pathParamKeys` / `req.pathParamValues`. + +### App + +```bux +let app: App = App_New(8080, 4); // port 8080, 4 worker threads +App_Run(&app); // blocks, handles requests +``` + +## Architecture + +``` +Incoming connection + │ + ▼ +Net_Accept() ─── worker thread (1 of N) + │ + ▼ +Net_Recv() → raw HTTP bytes + │ + ▼ +Request_Parse() → method, path, query, headers, body + │ + ▼ +Boko_Router(req) ←── YOUR CODE + │ + ▼ +Response_Build() → HTTP/1.1 response string + │ + ▼ +Net_Send() → bytes to client + │ + ▼ +Net_Close() +``` + +## Endpoints in Demo App + +| Method | Path | Description | +|--------|------|-------------| +| GET | `/` | Landing page (HTML) | +| GET | `/api/health` | Health check (JSON) | +| GET | `/api/info` | Framework info (JSON) | +| GET | `/hello?name=X` | Query param demo | +| GET | `/users/{id}` | Path param demo | +| GET | `/posts/{id}/comments/{cid}` | Multi path param demo | +| GET | `/redirect` | 302 redirect to `/` | +| POST | `/api/echo` | Echo request body | + +## Project Structure + +``` +apps/boko-framework/ +├── bux.toml # Package manifest +├── README.md # This file +└── src/ + ├── Boko.bux # Framework core (~500 lines) + └── Main.bux # Example app (~150 lines) +``` + +## Design Philosophy + +Boko is intentionally simple. Instead of complex DSLs or code generation, it gives you: + +- **One function to write** — `Boko_Router(req) -> Response` +- **Direct control** — you write plain if/else or match for routing +- **No magic** — path params, query params, headers are explicit function calls +- **Fast** — multi-threaded accept loop, zero allocations where possible + +This is the same philosophy as Go's `net/http` — simple, explicit, composable. + +## License + +Part of the Bux project. See root [LICENSE](../../LICENSE). diff --git a/apps/boko-framework/build.log b/apps/boko-framework/build.log new file mode 100644 index 0000000..903634c --- /dev/null +++ b/apps/boko-framework/build.log @@ -0,0 +1 @@ +EXIT:137 diff --git a/apps/boko-framework/build_output.log b/apps/boko-framework/build_output.log new file mode 100644 index 0000000..e69de29 diff --git a/apps/boko-framework/bux.toml b/apps/boko-framework/bux.toml new file mode 100644 index 0000000..57209c1 --- /dev/null +++ b/apps/boko-framework/bux.toml @@ -0,0 +1,8 @@ +[Package] +Name = "boko-framework" +Version = "0.1.0" +Type = "bin" +Description = "Boko — async web framework for Bux, inspired by FastAPI" + +[Build] +Output = "Bin" diff --git a/apps/boko-framework/check_output.log b/apps/boko-framework/check_output.log new file mode 100644 index 0000000..e69de29 diff --git a/apps/boko-framework/fresh.log b/apps/boko-framework/fresh.log new file mode 100644 index 0000000..903634c --- /dev/null +++ b/apps/boko-framework/fresh.log @@ -0,0 +1 @@ +EXIT:137 diff --git a/apps/boko-framework/lir2.log b/apps/boko-framework/lir2.log new file mode 100644 index 0000000..9148f69 --- /dev/null +++ b/apps/boko-framework/lir2.log @@ -0,0 +1 @@ +EXIT:143 diff --git a/apps/boko-framework/lir_check.log b/apps/boko-framework/lir_check.log new file mode 100644 index 0000000..903634c --- /dev/null +++ b/apps/boko-framework/lir_check.log @@ -0,0 +1 @@ +EXIT:137 diff --git a/apps/boko-framework/lir_demo.log b/apps/boko-framework/lir_demo.log new file mode 100644 index 0000000..79abcd7 --- /dev/null +++ b/apps/boko-framework/lir_demo.log @@ -0,0 +1,2 @@ +bash: ред 1: ../../buxc_lir: Няма такъв файл или директория +EXIT:127 diff --git a/apps/boko-framework/lir_demo2.log b/apps/boko-framework/lir_demo2.log new file mode 100644 index 0000000..9148f69 --- /dev/null +++ b/apps/boko-framework/lir_demo2.log @@ -0,0 +1 @@ +EXIT:143 diff --git a/apps/boko-framework/lir_demo3.log b/apps/boko-framework/lir_demo3.log new file mode 100644 index 0000000..9148f69 --- /dev/null +++ b/apps/boko-framework/lir_demo3.log @@ -0,0 +1 @@ +EXIT:143 diff --git a/apps/boko-framework/lir_ver.log b/apps/boko-framework/lir_ver.log new file mode 100644 index 0000000..3b4b4d6 --- /dev/null +++ b/apps/boko-framework/lir_ver.log @@ -0,0 +1,2 @@ +bux 0.1.0 (bootstrap) +EXIT:0 diff --git a/apps/boko-framework/make.log b/apps/boko-framework/make.log new file mode 100644 index 0000000..c1a4b03 --- /dev/null +++ b/apps/boko-framework/make.log @@ -0,0 +1,40 @@ +nim c -o:buxc -d:release --opt:size bootstrap/main.nim +Hint: used config file '/etc/nim/nim.cfg' [Conf] +Hint: used config file '/etc/nim/config.nims' [Conf] +........................................................................................................................................... +/home/ziko/z-git/bux/bux/bootstrap/lexer.nim(340, 12) Hint: 'check3' is declared but not used [XDeclaredButNotUsed] +/home/ziko/z-git/bux/bux/bootstrap/lexer.nim(350, 12) Hint: 'checkEq' is declared but not used [XDeclaredButNotUsed] +/home/ziko/z-git/bux/bux/bootstrap/lexer.nim(64, 6) Hint: 'match' is declared but not used [XDeclaredButNotUsed] +/home/ziko/z-git/bux/bux/bootstrap/lexer.nim(84, 6) Hint: 'emitWarning' is declared but not used [XDeclaredButNotUsed] +.. +/home/ziko/z-git/bux/bux/bootstrap/parser.nim(663, 7) Hint: 'loc' is declared but not used [XDeclaredButNotUsed] +/home/ziko/z-git/bux/bux/bootstrap/parser.nim(1001, 9) Hint: 'isVar' is declared but not used [XDeclaredButNotUsed] +/home/ziko/z-git/bux/bux/bootstrap/parser.nim(59, 6) Hint: 'match' is declared but not used [XDeclaredButNotUsed] +....... +/home/ziko/z-git/bux/bux/bootstrap/sema.nim(1150, 9) Hint: 'operandType' is declared but not used [XDeclaredButNotUsed] +/home/ziko/z-git/bux/bux/bootstrap/sema.nim(1155, 9) Hint: 'operandType' is declared but not used [XDeclaredButNotUsed] +/home/ziko/z-git/bux/bux/bootstrap/sema.nim(1165, 9) Hint: 'subjectType' is declared but not used [XDeclaredButNotUsed] +/home/ziko/z-git/bux/bux/bootstrap/sema.nim(1200, 9) Hint: 'operand' is declared but not used [XDeclaredButNotUsed] +/home/ziko/z-git/bux/bux/bootstrap/sema.nim(94, 6) Hint: 'emitWarning' is declared but not used [XDeclaredButNotUsed] +. +/home/ziko/z-git/bux/bux/bootstrap/manifest.nim(93, 11) Hint: 'val' is declared but not used [XDeclaredButNotUsed] +.. +/home/ziko/z-git/bux/bux/bootstrap/hir_lower.nim(1057, 9) Hint: 'loweredIter' is declared but not used [XDeclaredButNotUsed] +. +/home/ziko/z-git/bux/bux/bootstrap/lir.nim(5, 8) Warning: imported and not used: 'types' [UnusedImport] +. +/home/ziko/z-git/bux/bux/bootstrap/lir_lower.nim(419, 9) Hint: 't' is declared but not used [XDeclaredButNotUsed] +/home/ziko/z-git/bux/bux/bootstrap/lir_lower.nim(595, 9) Hint: 'bodyLbl' is declared but not used [XDeclaredButNotUsed] +/home/ziko/z-git/bux/bux/bootstrap/lir_lower.nim(734, 11) Hint: 'exprVal' is declared but not used [XDeclaredButNotUsed] +/home/ziko/z-git/bux/bux/bootstrap/lir_lower.nim(746, 9) Hint: 'exprVal' is declared but not used [XDeclaredButNotUsed] +/home/ziko/z-git/bux/bux/bootstrap/lir_lower.nim(6, 8) Warning: imported and not used: 'ast' [UnusedImport] +. +/home/ziko/z-git/bux/bux/bootstrap/lir_c_backend.nim(20, 6) Hint: 'emit' is declared but not used [XDeclaredButNotUsed] +/home/ziko/z-git/bux/bux/bootstrap/lir_c_backend.nim(46, 6) Hint: 'typeFromValue' is declared but not used [XDeclaredButNotUsed] +/home/ziko/z-git/bux/bux/bootstrap/lir_c_backend.nim(58, 6) Hint: 'setTempType' is declared but not used [XDeclaredButNotUsed] +/home/ziko/z-git/bux/bux/bootstrap/cli.nim(2, 55) Warning: imported and not used: 'lir' [UnusedImport] +Hint: [Link] +Hint: mm: orc; threads: on; opt: size; options: -d:release +76982 lines; 1.552s; 174.824MiB peakmem; proj: /home/ziko/z-git/bux/bux/bootstrap/main.nim; out: /home/ziko/z-git/bux/bux/buxc [SuccessX] +# strip buxc +EXIT:0 diff --git a/apps/boko-framework/run_check.sh b/apps/boko-framework/run_check.sh new file mode 100644 index 0000000..bff79e8 --- /dev/null +++ b/apps/boko-framework/run_check.sh @@ -0,0 +1,19 @@ +#!/bin/bash +cd /home/ziko/z-git/bux/bux/apps/boko-framework + +echo "=== CHECK ===" +../../buxc check 2>&1 +echo "CHECK_EXIT=$?" + +echo "" +echo "=== BUILD ===" +../../buxc build 2>&1 +echo "BUILD_EXIT=$?" + +echo "" +echo "=== FILES ===" +find . -type f -newer bux.toml 2>/dev/null | head -30 +echo "" +echo "=== BINARY SEARCH ===" +file boko-framework 2>/dev/null || echo "no boko-framework" +ls -la build/ 2>/dev/null || echo "no build dir" diff --git a/apps/boko-framework/src/Boko.bux b/apps/boko-framework/src/Boko.bux new file mode 100644 index 0000000..27ad29a --- /dev/null +++ b/apps/boko-framework/src/Boko.bux @@ -0,0 +1,507 @@ +// ============================================================================= +// Boko — Async Web Framework for Bux (inspired by FastAPI) +// ============================================================================= +module Boko { + +import Std::Io::{PrintLine, Print, PrintInt}; +import Std::Net::{Net_Create, Net_SetReuse, Net_Bind, Net_Listen, Net_Accept, Net_Send, Net_Recv, Net_Close, Net_LastError}; +import Std::String::{String_Len, String_Eq, String_StartsWith, String_Contains}; +import Std::Task::{Task_Join, TaskHandle}; + +extern func bux_alloc(size: uint) -> *void; +extern func bux_strlen(s: String) -> uint; +extern func bux_strcmp(a: String, b: String) -> int; +extern func bux_strstr(haystack: String, needle: String) -> String; +extern func bux_str_slice(s: String, start: uint, len: uint) -> String; +extern func bux_str_split_count(s: String, delim: String) -> uint; +extern func bux_str_split_part(s: String, delim: String, index: uint) -> String; +extern func bux_sb_new(initial_cap: uint) -> *void; +extern func bux_sb_append(sb: *void, s: String); +extern func bux_sb_append_int(sb: *void, n: int64); +extern func bux_sb_append_char(sb: *void, c: char8); +extern func bux_sb_build(sb: *void) -> String; +extern func bux_sb_free(sb: *void); +extern func bux_base64url_encode(data: String, len: int) -> String; +extern func bux_base64url_decode(data: String, len: int, outlen: *int) -> String; + +// ============================================================================= +// HTTP Methods +// ============================================================================= +enum HttpVerb { + GET, + POST, + PUT, + DELETE, + PATCH, + HEAD, + OPTIONS, +} + +// ============================================================================= +// Request — parsed incoming HTTP request +// ============================================================================= +struct Request { + method: HttpVerb, + path: String, + body: String, + headerKeys: *String, + headerValues: *String, + headerCount: int, + queryKeys: *String, + queryValues: *String, + queryCount: int, + pathParamKeys: *String, + pathParamValues: *String, + pathParamCount: int, +} + +// ============================================================================= +// Response — outgoing HTTP response +// ============================================================================= +struct Response { + statusCode: int, + contentType: String, + body: String, + extraHeaders: String, +} + +// ============================================================================= +// App — server instance +// ============================================================================= +struct App { + port: int, + threadCount: int, + serverName: String, +} + +// ============================================================================= +// Request helpers +// ============================================================================= + +func Request_GetHeader(req: *Request, name: String) -> String { + var i: int = 0; + while i < req.headerCount { + if bux_strcmp(req.headerKeys[i], name) == 0 { + return req.headerValues[i]; + } + i = i + 1; + } + return ""; +} + +func Request_GetQuery(req: *Request, name: String) -> String { + var i: int = 0; + while i < req.queryCount { + if bux_strcmp(req.queryKeys[i], name) == 0 { + return req.queryValues[i]; + } + i = i + 1; + } + return ""; +} + +func Request_GetPathParam(req: *Request, name: String) -> String { + var i: int = 0; + while i < req.pathParamCount { + if bux_strcmp(req.pathParamKeys[i], name) == 0 { + return req.pathParamValues[i]; + } + i = i + 1; + } + return ""; +} + +// ============================================================================= +// Response constructors +// ============================================================================= + +func Response_New(status: int, contentType: String, body: String) -> Response { + var resp: Response; + resp.statusCode = status; + resp.contentType = contentType; + resp.body = body; + resp.extraHeaders = ""; + return resp; +} + +func Response_Ok(body: String) -> Response { + return Response_New(200, "text/html; charset=utf-8", body); +} + +func Response_Html(html: String) -> Response { + return Response_New(200, "text/html; charset=utf-8", html); +} + +func Response_Json(json: String) -> Response { + return Response_New(200, "application/json; charset=utf-8", json); +} + +func Response_Text(text: String) -> Response { + return Response_New(200, "text/plain; charset=utf-8", text); +} + +func Response_Redirect(url: String) -> Response { + var resp: Response; + resp.statusCode = 302; + resp.contentType = ""; + resp.body = ""; + let sb: *void = bux_sb_new(256); + bux_sb_append(sb, "Location: "); + bux_sb_append(sb, url); + bux_sb_append(sb, "\r\n"); + resp.extraHeaders = bux_sb_build(sb); + bux_sb_free(sb); + return resp; +} + +func Response_NotFound() -> Response { + return Response_New(404, "application/json; charset=utf-8", + "{\"error\":\"not_found\"}"); +} + +func Response_Error(status: int, message: String) -> Response { + return Response_New(status, "application/json; charset=utf-8", message); +} + +func Response_NoContent() -> Response { + return Response_New(204, "", ""); +} + +// ============================================================================= +// Build HTTP response string from Response struct +// ============================================================================= +func Response_Build(resp: Response) -> String { + let sb: *void = bux_sb_new(2048); + + // Status line + bux_sb_append(sb, "HTTP/1.1 "); + bux_sb_append_int(sb, resp.statusCode as int64); + bux_sb_append(sb, " "); + if resp.statusCode == 200 { bux_sb_append(sb, "OK"); } + else if resp.statusCode == 201 { bux_sb_append(sb, "Created"); } + else if resp.statusCode == 204 { bux_sb_append(sb, "No Content"); } + else if resp.statusCode == 302 { bux_sb_append(sb, "Found"); } + else if resp.statusCode == 400 { bux_sb_append(sb, "Bad Request"); } + else if resp.statusCode == 404 { bux_sb_append(sb, "Not Found"); } + else if resp.statusCode == 500 { bux_sb_append(sb, "Internal Server Error"); } + else { bux_sb_append(sb, "OK"); } + bux_sb_append(sb, "\r\n"); + + // Server + bux_sb_append(sb, "Server: Boko/0.1.0 (Bux)\r\n"); + + // Extra headers (redirects, CORS, etc.) + if bux_strlen(resp.extraHeaders) > 0 { + bux_sb_append(sb, resp.extraHeaders); + } + + // Content-Type + if bux_strlen(resp.contentType) > 0 { + bux_sb_append(sb, "Content-Type: "); + bux_sb_append(sb, resp.contentType); + bux_sb_append(sb, "\r\n"); + } + + // Content-Length + let bodyLen: uint = bux_strlen(resp.body); + bux_sb_append(sb, "Content-Length: "); + bux_sb_append_int(sb, bodyLen as int64); + bux_sb_append(sb, "\r\n"); + + // Connection close + bux_sb_append(sb, "Connection: close\r\n"); + + bux_sb_append(sb, "\r\n"); + + if bodyLen > 0 { + bux_sb_append(sb, resp.body); + } + + let result: String = bux_sb_build(sb); + bux_sb_free(sb); + return result; +} + +// ============================================================================= +// Parse query string: ?key=val&key2=val2 → key-value arrays +// ============================================================================= +func Query_Parse(req: *Request, queryString: String) { + if bux_strlen(queryString) == 0 { return; } + let pairCount: uint = bux_str_split_count(queryString, "&"); + let cap: int = pairCount as int + 1; + req.queryKeys = bux_alloc(cap as uint * 8) as *String; + req.queryValues = bux_alloc(cap as uint * 8) as *String; + req.queryCount = 0; + + var i: uint = 0; + while i < pairCount { + let pair: String = bux_str_split_part(queryString, "&", i); + let eqPos: String = bux_strstr(pair, "="); + var key: String = pair; + var value: String = ""; + if eqPos as uint != 0 { + let keyLen: uint = eqPos as uint - pair as uint; + key = bux_str_slice(pair, 0, keyLen); + let valStart: uint = keyLen + 1; + let pairLen: uint = bux_strlen(pair); + if valStart < pairLen { + value = bux_str_slice(pair, valStart, pairLen - valStart); + } + } + req.queryKeys[req.queryCount] = key; + req.queryValues[req.queryCount] = value; + req.queryCount = req.queryCount + 1; + i = i + 1; + } +} + +// ============================================================================= +// Parse incoming HTTP request from raw bytes +// ============================================================================= +func Request_Parse(raw: String) -> Request { + var req: Request; + req.method = HttpVerb { tag: HttpVerb_GET }; + req.path = "/"; + req.body = ""; + req.headerCount = 0; + req.queryCount = 0; + req.pathParamCount = 0; + + if raw as uint == 0 { return req; } + let rawLen: uint = bux_strlen(raw); + if rawLen == 0 { return req; } + + // Find header/body boundary + let boundary: String = bux_strstr(raw, "\r\n\r\n"); + var headerLen: uint = rawLen; + if boundary as uint != 0 { + headerLen = boundary as uint - raw as uint; + let bodyStart: uint = headerLen + 4; + if bodyStart < rawLen { + req.body = bux_str_slice(raw, bodyStart, rawLen - bodyStart); + } + } + + if headerLen == 0 { return req; } + let headerBlock: String = bux_str_slice(raw, 0, headerLen); + let lineCount: uint = bux_str_split_count(headerBlock, "\r\n"); + if lineCount == 0 { return req; } + + // Parse request line + let requestLine: String = bux_str_split_part(headerBlock, "\r\n", 0); + if bux_strlen(requestLine) > 0 { + let methodStr: String = bux_str_split_part(requestLine, " ", 0); + if String_Eq(methodStr, "GET") { req.method = HttpVerb { tag: HttpVerb_GET }; } + else if String_Eq(methodStr, "POST") { req.method = HttpVerb { tag: HttpVerb_POST }; } + else if String_Eq(methodStr, "PUT") { req.method = HttpVerb { tag: HttpVerb_PUT }; } + else if String_Eq(methodStr, "DELETE") { req.method = HttpVerb { tag: HttpVerb_DELETE }; } + + let fullPath: String = bux_str_split_part(requestLine, " ", 1); + // Split path and query string + let qmark: String = bux_strstr(fullPath, "?"); + if qmark as uint != 0 { + let pathLen: uint = qmark as uint - fullPath as uint; + req.path = bux_str_slice(fullPath, 0, pathLen); + let qsStart: uint = pathLen + 1; + let fullLen: uint = bux_strlen(fullPath); + if qsStart < fullLen { + let qs: String = bux_str_slice(fullPath, qsStart, fullLen - qsStart); + Query_Parse(&req, qs); + } + } else { + req.path = fullPath; + } + } + + // Parse headers + let cap: int = lineCount as int + 1; + req.headerKeys = bux_alloc(cap as uint * 8) as *String; + req.headerValues = bux_alloc(cap as uint * 8) as *String; + + var i: uint = 1; + while i < lineCount { + let line: String = bux_str_split_part(headerBlock, "\r\n", i); + let colonPos: String = bux_strstr(line, ": "); + if colonPos as uint != 0 { + let keyLen: uint = colonPos as uint - line as uint; + let key: String = bux_str_slice(line, 0, keyLen); + let valStart: uint = keyLen + 2; + let lineLen: uint = bux_strlen(line); + var value: String = ""; + if valStart < lineLen { + value = bux_str_slice(line, valStart, lineLen - valStart); + } + req.headerKeys[req.headerCount] = key; + req.headerValues[req.headerCount] = value; + req.headerCount = req.headerCount + 1; + } + i = i + 1; + } + + return req; +} + +// ============================================================================= +// Path pattern matching: /users/{id} against /users/42 → extracts id=42 +// ============================================================================= +func Path_Match(pattern: String, path: String, req: *Request) -> bool { + // Split both by "/" + let patParts: uint = bux_str_split_count(pattern, "/"); + let pathParts: uint = bux_str_split_count(path, "/"); + + if patParts != pathParts { return false; } + + // Count params to allocate + var paramCount: int = 0; + var pi: uint = 0; + while pi < patParts { + let patPart: String = bux_str_split_part(pattern, "/", pi); + if String_StartsWith(patPart, "{") && String_Contains(patPart, "}") { + paramCount = paramCount + 1; + } + pi = pi + 1; + } + + let cap: int = paramCount + 1; + req.pathParamKeys = bux_alloc(cap as uint * 8) as *String; + req.pathParamValues = bux_alloc(cap as uint * 8) as *String; + req.pathParamCount = 0; + + // Match and extract + var i: uint = 0; + while i < patParts { + let patPart: String = bux_str_split_part(pattern, "/", i); + let pathPart: String = bux_str_split_part(path, "/", i); + + if String_StartsWith(patPart, "{") && String_Contains(patPart, "}") { + // Extract param name from {name} + let braceOpen: uint = 1; // skip "{" + let braceClose: String = bux_strstr(patPart, "}"); + var nameLen: uint = 0; + if braceClose as uint != 0 { + nameLen = braceClose as uint - patPart as uint - 1; + } + let name: String = bux_str_slice(patPart, braceOpen, nameLen); + req.pathParamKeys[req.pathParamCount] = name; + req.pathParamValues[req.pathParamCount] = pathPart; + req.pathParamCount = req.pathParamCount + 1; + } else { + // Literal match + if !String_Eq(patPart, pathPart) { return false; } + } + i = i + 1; + } + + return true; +} + +// ============================================================================= +// App constructor +// ============================================================================= +func App_New(port: int, threadCount: int) -> App { + var app: App; + app.port = port; + app.threadCount = threadCount; + app.serverName = "Boko/0.1.0 (Bux)"; + return app; +} + +// ============================================================================= +// Forward declaration — user must implement this in their module +// ============================================================================= +func Boko_Router(req: Request) -> Response; + +// ============================================================================= +// Handle a single connection: parse → dispatch → respond +// ============================================================================= +func App_HandleConnection(clientFd: int) { + let raw: String = Net_Recv(clientFd, 8192); + if raw as uint == 0 { return; } + if bux_strlen(raw) == 0 { return; } + + let req: Request = Request_Parse(raw); + + // Call user-defined dispatch (forward declaration — user must implement) + let resp: Response = Boko_Router(req); + + // Log + if req.method.tag == HttpVerb_GET { Print("GET "); } + else if req.method.tag == HttpVerb_POST { Print("POST "); } + else if req.method.tag == HttpVerb_PUT { Print("PUT "); } + else if req.method.tag == HttpVerb_DELETE { Print("DELETE "); } + else { Print("? "); } + Print(req.path); + Print(" → "); + PrintInt(resp.statusCode); + PrintLine(""); + + // Send + let respStr: String = Response_Build(resp); + Net_Send(clientFd, respStr); +} + +// ============================================================================= +// Worker thread: accept loop +// ============================================================================= +func App_Worker(serverFd: int) { + while true { + let clientFd: int = Net_Accept(serverFd); + if clientFd < 0 { continue; } + App_HandleConnection(clientFd); + Net_Close(clientFd); + } +} + +// ============================================================================= +// App_Run: start the server (blocking) +// ============================================================================= +func App_Run(app: *App) { + PrintLine("╔══════════════════════════════════════╗"); + PrintLine("║ Boko Framework v0.1.0 ║"); + PrintLine("║ Async web framework for Bux ║"); + PrintLine("╚══════════════════════════════════════╝"); + PrintLine(""); + + let fd: int = Net_Create(); + if fd < 0 { + PrintLine("FATAL: socket() failed"); + return; + } + + Net_SetReuse(fd); + + if !Net_Bind(fd, "0.0.0.0", app.port) { + Print("FATAL: bind(:"); + PrintInt(app.port); + Print(") failed: "); + PrintLine(Net_LastError()); + Net_Close(fd); + return; + } + + if !Net_Listen(fd, 128) { + PrintLine("FATAL: listen() failed"); + Net_Close(fd); + return; + } + + Print("✓ Boko running on http://0.0.0.0:"); + PrintInt(app.port); + PrintLine(""); + Print("✓ Workers: "); + PrintInt(app.threadCount); + PrintLine(""); + PrintLine(""); + + // Spawn workers + var i: int = 0; + while i < app.threadCount - 1 { + spawn App_Worker(fd); + i = i + 1; + } + + // Main thread is the last worker + App_Worker(fd); +} + +} // module Boko diff --git a/apps/boko-framework/src/Main.bux b/apps/boko-framework/src/Main.bux new file mode 100644 index 0000000..ab58839 --- /dev/null +++ b/apps/boko-framework/src/Main.bux @@ -0,0 +1,160 @@ +// ============================================================================= +// Boko Example App — demonstrates the framework API +// ============================================================================= +module Main { + +import Std::Io::{PrintLine, Print}; +import Std::String::{String_Eq}; +import Boko::{ + App, App_New, App_Run, + Request, Response, + Request_GetQuery, Request_GetPathParam, + Response_Html, Response_Json, Response_NotFound, Response_Redirect, + Path_Match, + HttpVerb +}; + +extern func bux_strlen(s: String) -> uint; +extern func bux_sb_new(initial_cap: uint) -> *void; +extern func bux_sb_append(sb: *void, s: String); +extern func bux_sb_build(sb: *void) -> String; +extern func bux_sb_free(sb: *void); + +// ============================================================================= +// Boko_Router — user-defined dispatch (called by the framework) +// ============================================================================= +func Boko_Router(req: Request) -> Response { + // --- GET / --- + if String_Eq(req.path, "/") && req.method.tag == HttpVerb_GET { + return Response_Html(PageHome()); + } + + // --- GET /api/health --- + if String_Eq(req.path, "/api/health") { + return Response_Json("{\"status\":\"ok\",\"framework\":\"Boko\",\"version\":\"0.1.0\"}"); + } + + // --- GET /api/info --- + if String_Eq(req.path, "/api/info") { + return Response_Json("{\"name\":\"Boko\",\"language\":\"Bux\",\"inspiration\":\"FastAPI\",\"features\":[\"routing\",\"path-params\",\"query-params\",\"json\"]}"); + } + + // --- GET /hello?name=World --- + if String_Eq(req.path, "/hello") { + let name: String = Request_GetQuery(&req, "name"); + if bux_strlen(name) == 0 { name = "World"; } + let sb: *void = bux_sb_new(256); + bux_sb_append(sb, "

Hello, "); + bux_sb_append(sb, name); + bux_sb_append(sb, "!

"); + let html: String = bux_sb_build(sb); + bux_sb_free(sb); + return Response_Html(html); + } + + // --- GET /users/{id} --- + if Path_Match("/users/{id}", req.path, &req) { + let id: String = Request_GetPathParam(&req, "id"); + let sb: *void = bux_sb_new(128); + bux_sb_append(sb, "{\"id\":"); + bux_sb_append(sb, id); + bux_sb_append(sb, ",\"name\":\"User "); + bux_sb_append(sb, id); + bux_sb_append(sb, "\"}"); + let json: String = bux_sb_build(sb); + bux_sb_free(sb); + return Response_Json(json); + } + + // --- GET /posts/{postId}/comments/{commentId} --- + if Path_Match("/posts/{postId}/comments/{commentId}", req.path, &req) { + let pid: String = Request_GetPathParam(&req, "postId"); + let cid: String = Request_GetPathParam(&req, "commentId"); + let sb: *void = bux_sb_new(128); + bux_sb_append(sb, "{\"postId\":"); + bux_sb_append(sb, pid); + bux_sb_append(sb, ",\"commentId\":"); + bux_sb_append(sb, cid); + bux_sb_append(sb, ",\"text\":\"Great post!\"}"); + let json: String = bux_sb_build(sb); + bux_sb_free(sb); + return Response_Json(json); + } + + // --- GET /redirect → 302 to / --- + if String_Eq(req.path, "/redirect") { + return Response_Redirect("/"); + } + + // --- POST /api/echo --- + if String_Eq(req.path, "/api/echo") && req.method.tag == HttpVerb_POST { + let sb: *void = bux_sb_new(256); + bux_sb_append(sb, "{\"echo\":"); + bux_sb_append(sb, req.body); + bux_sb_append(sb, "}"); + let json: String = bux_sb_build(sb); + bux_sb_free(sb); + return Response_Json(json); + } + + // --- 404 --- + return Response_NotFound(); +} + +// ============================================================================= +// HTML landing page (raw multi-line string via backticks) +// ============================================================================= +func PageHome() -> String { + return ` + + + + +Boko Framework + + + +
+

⚡ Boko

+

Async web framework for Bux — inspired by FastAPI

+

Try it

+
GET/hello?name=Buxquery param
+
GET/users/42path param
+
GET/posts/7/comments/3multi params
+
GET/redirect302 → /
+
GET/api/healthJSON
+
GET/api/infoJSON
+

Features

+
Path routing with {params}
+
Query parameter extraction
+
JSON / HTML / Text responses
+
Multi-threaded (configurable)
+
Redirects (302)
+
POST body access
+
+ +`; +} + +// ============================================================================= +// Main — start the server +// ============================================================================= +func Main() -> int { + let app: App = App_New(8080, 4); + App_Run(&app); + return 0; +} + +} // module Main diff --git a/apps/jwt-pitbul/README.md b/apps/jwt-pitbul/README.md new file mode 100644 index 0000000..24a5e25 --- /dev/null +++ b/apps/jwt-pitbul/README.md @@ -0,0 +1,175 @@ +# jwt-pitbul + +**JWT CLI tool for the Bux ecosystem — sign, verify, decode, and key generation.** + +A standalone command-line utility for working with JSON Web Tokens (RFC 7519). Built on `Std::Crypto`, backed by OpenSSL. + +## Quick Start + +```bash +cd apps/jwt-pitbul +../../buxc build +./jwt-pitbul +``` + +## Commands + +### `sign` — Create a JWT + +``` +jwt-pitbul sign +``` + +| Argument | Description | +|----------|-------------| +| `` | Algorithm: `HS256`, `HS384`, `HS512`, `RS256`, `RS384`, `RS512`, `ES256`, `ES384`, `EdDSA` | +| `` | HMAC secret string, or path to PEM file (RSA/ECDSA), or base64 raw key (Ed25519) | +| `` | JSON payload string (e.g. `{"sub":"123","exp":1735689600}`) | + +```bash +# HMAC +jwt-pitbul sign HS256 'my-secret-key' '{"sub":"123","role":"admin"}' + +# RSA (private key from file) +jwt-pitbul sign RS256 private.pem '{"sub":"456"}' + +# ECDSA +jwt-pitbul sign ES256 ec_private.pem '{"sub":"789"}' + +# Ed25519 (base64-encoded 32-byte private key) +jwt-pitbul sign EdDSA 'MC4CAQAwBQYDK2VwBCIEIJ...' '{"sub":"000"}' +``` + +### `verify` — Verify and decode + +``` +jwt-pitbul verify +``` + +```bash +jwt-pitbul verify eyJhbGciOiJ... HS256 'my-secret-key' +# ✓ Signature valid +# +# Header: +# {"alg":"HS256","typ":"JWT"} +# +# Payload: +# {"sub":"123","role":"admin"} +``` + +### `decode` — Decode without verification + +``` +jwt-pitbul decode +``` + +```bash +jwt-pitbul decode eyJhbGciOiJ... +# Decoded (no verification): +# +# Header: +# {"alg":"HS256","typ":"JWT"} +# +# Payload: +# {"sub":"123"} +# +# Signature (base64url): xxxxxxxxx... +``` + +### `keygen` — Generate keys + +``` +jwt-pitbul keygen +``` + +| Type | Output | +|------|--------| +| `ed25519` | Generates an Ed25519 keypair (prints base64 public/private keys) | +| `rsa` | Prints OpenSSL CLI commands to generate RSA 2048-bit keys | +| `ecdsa` | Prints OpenSSL CLI commands to generate ECDSA P-256 keys | + +```bash +jwt-pitbul keygen ed25519 +# Ed25519 keypair (base64): +# Public: MCowBQYDK2VwAyEA... +# Private: MC4CAQAwBQYDK2VwBCIEIJ... + +jwt-pitbul keygen rsa +# RSA key generation requires OpenSSL CLI: +# openssl genpkey -algorithm RSA -out private.pem -pkeyopt rsa_keygen_bits:2048 +# openssl rsa -in private.pem -pubout -out public.pem +``` + +## Supported Algorithms + +| Algorithm | Type | Key Format | +|-----------|------|------------| +| HS256 | HMAC-SHA256 | Raw secret string | +| HS384 | HMAC-SHA384 | Raw secret string | +| HS512 | HMAC-SHA512 | Raw secret string | +| RS256 | RSA PKCS#1 v1.5 SHA-256 | PEM file path | +| RS384 | RSA PKCS#1 v1.5 SHA-384 | PEM file path | +| RS512 | RSA PKCS#1 v1.5 SHA-512 | PEM file path | +| ES256 | ECDSA P-256 | PEM file path | +| ES384 | ECDSA P-384 | PEM file path | +| EdDSA | Ed25519 | Base64 32-byte raw key | + +## Generating Keys with OpenSSL + +### RSA 2048-bit + +```bash +openssl genpkey -algorithm RSA -out private.pem -pkeyopt rsa_keygen_bits:2048 +openssl rsa -in private.pem -pubout -out public.pem +``` + +### ECDSA P-256 + +```bash +openssl ecparam -genkey -name prime256v1 -noout -out ec_private.pem +openssl ec -in ec_private.pem -pubout -out ec_public.pem +``` + +### ECDSA P-384 + +```bash +openssl ecparam -genkey -name secp384r1 -noout -out ec_private.pem +openssl ec -in ec_private.pem -pubout -out ec_public.pem +``` + +### Ed25519 + +```bash +# In-app generation: +jwt-pitbul keygen ed25519 + +# Or via OpenSSL: +openssl genpkey -algorithm Ed25519 -out ed25519_private.pem +openssl pkey -in ed25519_private.pem -pubout -out ed25519_public.pem +``` + +## Error Codes + +| Exit | Meaning | +|------|---------| +| 0 | Success | +| 1 | Error (invalid arguments, bad signature, missing file, etc.) | + +## Building + +```bash +cd apps/jwt-pitbul +../../buxc build # Bootstrap compiler (Nim) +# or +../../buxc_lir build # Self-hosted compiler +``` + +## Dependencies + +- Bux standard library (`Std::Crypto`, `Std::Io`, `Std::String`) +- OpenSSL 1.1.1+ (linked via runtime) +- Bux compiler (`buxc` or `buxc_lir`) + +## License + +Part of the Bux project. See root [LICENSE](../../LICENSE). diff --git a/apps/jwt-pitbul/bux.toml b/apps/jwt-pitbul/bux.toml new file mode 100644 index 0000000..50827c4 --- /dev/null +++ b/apps/jwt-pitbul/bux.toml @@ -0,0 +1,8 @@ +[Package] +Name = "jwt-pitbul" +Version = "0.1.0" +Type = "bin" +Description = "JWT CLI tool — encode, decode, verify, and key generation" + +[Build] +Output = "Bin" diff --git a/apps/jwt-pitbul/src/Main.bux b/apps/jwt-pitbul/src/Main.bux new file mode 100644 index 0000000..2f4e0ac --- /dev/null +++ b/apps/jwt-pitbul/src/Main.bux @@ -0,0 +1,326 @@ +// ============================================================================= +// jwt-pitbul — JWT CLI Tool (encode, decode, verify, keygen) +// Built with Bux + Std::Crypto +// ============================================================================= +module Main { + +import Std::Io::{PrintLine, Print, PrintInt}; +import Std::String::{String_Len, String_Eq, String_Contains}; +import Std::Crypto::Jwt::{ + JwtAlg, + Jwt_MakeHeader, + Jwt_Encode, + Jwt_Decode, + Jwt_EncodeHS256, Jwt_EncodeHS384, Jwt_EncodeHS512, + Jwt_EncodeRS256, Jwt_EncodeES256, Jwt_EncodeEdDSA +}; +import Std::Crypto::Base64::{Base64URL_Decode, Base64_Encode}; +import Std::Crypto::Ed25519::{Ed25519_Keypair}; + +extern func bux_argc() -> int; +extern func bux_argv(index: int) -> String; +extern func bux_alloc(size: uint) -> *void; +extern func bux_read_file(path: String) -> String; +extern func bux_file_exists(path: String) -> int; +extern func bux_str_split_part(s: String, delim: String, index: uint) -> String; +extern func bux_str_split_count(s: String, delim: String) -> uint; +extern func bux_base64_encode(data: String, len: int) -> String; + +// ============================================================================= +// Help / Usage +// ============================================================================= +func PrintUsage() { + PrintLine("╔══════════════════════════════════════════════════════╗"); + PrintLine("║ jwt-pitbul — JWT CLI Tool v0.1.0 ║"); + PrintLine("║ Sign, verify, decode JSON Web Tokens ║"); + PrintLine("╚══════════════════════════════════════════════════════╝"); + PrintLine(""); + PrintLine("Usage:"); + PrintLine(" jwt-pitbul sign "); + PrintLine(" jwt-pitbul verify "); + PrintLine(" jwt-pitbul decode "); + PrintLine(" jwt-pitbul keygen "); + PrintLine(""); + PrintLine("Commands:"); + PrintLine(" sign Create a signed JWT from claims JSON"); + PrintLine(" verify Verify a JWT signature and print payload"); + PrintLine(" decode Decode a JWT without verification"); + PrintLine(" keygen Generate cryptographic keys"); + PrintLine(""); + PrintLine("Algorithms:"); + PrintLine(" HS256, HS384, HS512 — HMAC (symmetric)"); + PrintLine(" RS256, RS384, RS512 — RSA PKCS#1 v1.5"); + PrintLine(" ES256, ES384 — ECDSA P-256 / P-384"); + PrintLine(" EdDSA — Ed25519"); + PrintLine(""); + PrintLine("Key formats:"); + PrintLine(" HMAC: raw secret string"); + PrintLine(" RSA: path to PEM private/public key file"); + PrintLine(" ECDSA: path to PEM private/public key file"); + PrintLine(" EdDSA: base64-encoded 32-byte raw key"); + PrintLine(""); + PrintLine("Key generation:"); + PrintLine(" jwt-pitbul keygen rsa # RSA 2048-bit (PEM)"); + PrintLine(" jwt-pitbul keygen ecdsa # ECDSA P-256 (PEM)"); + PrintLine(" jwt-pitbul keygen ed25519 # Ed25519 (raw base64)"); + PrintLine(""); + PrintLine("Examples:"); + PrintLine(" jwt-pitbul sign HS256 'my-secret' '{\"sub\":\"123\"}'"); + PrintLine(" jwt-pitbul verify eyJh... HS256 'my-secret'"); + PrintLine(" jwt-pitbul decode eyJh..."); + PrintLine(" jwt-pitbul keygen ed25519"); +} + +// ============================================================================= +// Algorithm name → JwtAlg enum +// ============================================================================= +func ParseAlg(name: String) -> JwtAlg { + if String_Eq(name, "HS256") { return JwtAlg { tag: JwtAlg_HS256 }; } + if String_Eq(name, "HS384") { return JwtAlg { tag: JwtAlg_HS384 }; } + if String_Eq(name, "HS512") { return JwtAlg { tag: JwtAlg_HS512 }; } + if String_Eq(name, "RS256") { return JwtAlg { tag: JwtAlg_RS256 }; } + if String_Eq(name, "RS384") { return JwtAlg { tag: JwtAlg_RS384 }; } + if String_Eq(name, "RS512") { return JwtAlg { tag: JwtAlg_RS512 }; } + if String_Eq(name, "ES256") { return JwtAlg { tag: JwtAlg_ES256 }; } + if String_Eq(name, "ES384") { return JwtAlg { tag: JwtAlg_ES384 }; } + if String_Eq(name, "EdDSA") { return JwtAlg { tag: JwtAlg_EdDSA }; } + return JwtAlg { tag: JwtAlg_HS256 }; +} + +// ============================================================================= +// Resolve key — for RSA/ECDSA, read PEM file; for HMAC/EdDSA, pass through +// ============================================================================= +func ResolveKey(alg: JwtAlg, keyArg: String) -> String { + let algTag: int = alg.tag; + + // RSA and ECDSA: key is a path to PEM file + if algTag == JwtAlg_RS256 || algTag == JwtAlg_RS384 || algTag == JwtAlg_RS512 || + algTag == JwtAlg_ES256 || algTag == JwtAlg_ES384 { + if bux_file_exists(keyArg) != 0 { + let pem: String = bux_read_file(keyArg); + if pem as uint != 0 && String_Len(pem) > 0 { + return pem; + } + Print("ERROR: could not read PEM file: "); + PrintLine(keyArg); + return ""; + } + Print("ERROR: PEM file not found: "); + PrintLine(keyArg); + return ""; + } + + // HMAC and EdDSA: key is used directly + return keyArg; +} + +// ============================================================================= +// Check if algorithm is symmetric (HMAC) or asymmetric +// ============================================================================= +func IsSymmetric(alg: JwtAlg) -> bool { + let t: int = alg.tag; + return t == JwtAlg_HS256 || t == JwtAlg_HS384 || t == JwtAlg_HS512; +} + +// ============================================================================= +// Command: sign +// ============================================================================= +func CmdSign(algName: String, keyArg: String, claimsJson: String) -> int { + let alg: JwtAlg = ParseAlg(algName); + let key: String = ResolveKey(alg, keyArg); + if String_Len(key) == 0 { + return 1; + } + + let header: String = Jwt_MakeHeader(alg); + let token: String = Jwt_Encode(header, claimsJson, alg, key); + + if String_Len(token) == 0 { + PrintLine("ERROR: signing failed"); + return 1; + } + + PrintLine(token); + return 0; +} + +// ============================================================================= +// Command: verify +// ============================================================================= +func CmdVerify(token: String, algName: String, keyArg: String) -> int { + let alg: JwtAlg = ParseAlg(algName); + let key: String = ResolveKey(alg, keyArg); + if String_Len(key) == 0 { + return 1; + } + + var header: String = ""; + var payload: String = ""; + + let ok: bool = Jwt_Decode(token, alg, key, &header, &payload); + + if ok { + PrintLine("✓ Signature valid"); + PrintLine(""); + PrintLine("Header:"); + PrintLine(header); + PrintLine(""); + PrintLine("Payload:"); + PrintLine(payload); + return 0; + } + + PrintLine("✗ Signature INVALID (or malformed token)"); + return 1; +} + +// ============================================================================= +// Command: decode (no verification) +// ============================================================================= +func CmdDecode(token: String) -> int { + let partCount: uint = bux_str_split_count(token, "."); + if partCount != 3 { + PrintLine("ERROR: not a valid JWT (expected 3 parts)"); + return 1; + } + + let headerB64: String = bux_str_split_part(token, ".", 0); + let payloadB64: String = bux_str_split_part(token, ".", 1); + let sigB64: String = bux_str_split_part(token, ".", 2); + + let headerJson: String = Base64URL_Decode(headerB64); + let payloadJson: String = Base64URL_Decode(payloadB64); + + PrintLine("Decoded (no verification):"); + PrintLine(""); + PrintLine("Header:"); + PrintLine(headerJson); + PrintLine(""); + PrintLine("Payload:"); + PrintLine(payloadJson); + PrintLine(""); + Print("Signature (base64url): "); + PrintLine(sigB64); + + return 0; +} + +// ============================================================================= +// Command: keygen +// ============================================================================= +func CmdKeygen(keyType: String) -> int { + if String_Eq(keyType, "ed25519") { + let pub: *void = bux_alloc(32); + let priv: *void = bux_alloc(32); + if !Ed25519_Keypair(pub, priv) { + PrintLine("ERROR: Ed25519 key generation failed (OpenSSL 1.1.1+ required)"); + return 1; + } + let pubB64: String = bux_base64_encode(pub as String, 32); + let privB64: String = bux_base64_encode(priv as String, 32); + PrintLine("Ed25519 keypair (base64):"); + Print(" Public: "); + PrintLine(pubB64); + Print(" Private: "); + PrintLine(privB64); + return 0; + } + + if String_Eq(keyType, "rsa") { + PrintLine("RSA key generation requires OpenSSL CLI:"); + PrintLine(" openssl genpkey -algorithm RSA -out private.pem -pkeyopt rsa_keygen_bits:2048"); + PrintLine(" openssl rsa -in private.pem -pubout -out public.pem"); + PrintLine(""); + PrintLine("(Key generation from within Bux requires PEM write support — coming soon)"); + return 0; + } + + if String_Eq(keyType, "ecdsa") { + PrintLine("ECDSA key generation requires OpenSSL CLI:"); + PrintLine(" openssl ecparam -genkey -name prime256v1 -noout -out ec_private.pem"); + PrintLine(" openssl ec -in ec_private.pem -pubout -out ec_public.pem"); + PrintLine(""); + PrintLine("(Key generation from within Bux requires PEM write support — coming soon)"); + return 0; + } + + Print("ERROR: unknown key type '"); + Print(keyType); + PrintLine("'. Use: rsa, ecdsa, ed25519"); + return 1; +} + +// ============================================================================= +// Main Entry Point +// ============================================================================= +func Main() -> int { + let argc: int = bux_argc(); + + if argc < 2 { + PrintUsage(); + return 0; + } + + // Collect arguments + let args: *String = bux_alloc(argc as uint * 8) as *String; + var i: int = 0; + while i < argc { + args[i] = bux_argv(i); + i = i + 1; + } + + let command: String = args[1]; + + if String_Eq(command, "help") || String_Eq(command, "--help") || String_Eq(command, "-h") { + PrintUsage(); + return 0; + } + + // --- sign --- + if String_Eq(command, "sign") { + if argc < 5 { + PrintLine("ERROR: 'sign' requires: "); + PrintLine(" jwt-pitbul sign HS256 'secret' '{\"sub\":\"123\"}'"); + return 1; + } + return CmdSign(args[2], args[3], args[4]); + } + + // --- verify --- + if String_Eq(command, "verify") { + if argc < 5 { + PrintLine("ERROR: 'verify' requires: "); + PrintLine(" jwt-pitbul verify eyJh... HS256 'secret'"); + return 1; + } + return CmdVerify(args[2], args[3], args[4]); + } + + // --- decode --- + if String_Eq(command, "decode") { + if argc < 3 { + PrintLine("ERROR: 'decode' requires: "); + PrintLine(" jwt-pitbul decode eyJh..."); + return 1; + } + return CmdDecode(args[2]); + } + + // --- keygen --- + if String_Eq(command, "keygen") { + if argc < 3 { + PrintLine("ERROR: 'keygen' requires: "); + PrintLine(" jwt-pitbul keygen ed25519"); + return 1; + } + return CmdKeygen(args[3]); + } + + Print("ERROR: unknown command '"); + Print(command); + PrintLine("'"); + PrintLine("Run 'jwt-pitbul help' for usage."); + return 1; +} + +} // module Main diff --git a/apps/nexus/README.md b/apps/nexus/README.md new file mode 100644 index 0000000..260f994 --- /dev/null +++ b/apps/nexus/README.md @@ -0,0 +1,189 @@ +# Nexus + +**High-performance, multi-threaded HTTP/1.1, HTTP/2 & WebSocket server — built with the [Bux](https://github.com/bux-lang/bux) programming language.** + +Nexus is a from-scratch web server that demonstrates Bux's systems-programming capabilities: raw TCP sockets, pthread-based concurrency, manual memory management, and zero-dependency C ABI interop — all from a clean, modern syntax. + +--- + +## Features + +| Area | What's Implemented | +|------|-------------------| +| **HTTP/1.1** | Full request parsing (method, path, headers, body), response building with status codes, content negotiation | +| **Multi-threaded** | Configurable worker pool using the multi-accept pattern — each worker calls `accept()` directly on the shared listen socket | +| **HTTP/2** | Connection preface detection (`PRI * HTTP/2.0`), upgrade-aware routing | +| **WebSocket** | RFC 6455 upgrade handshake detection, `Sec-WebSocket-Key` extraction | +| **Static files** | Serves from `public/` with MIME-type detection for 20+ file types, directory-traversal protection | +| **JSON API** | Built-in `/api/health` and `/api/info` endpoints | +| **Logging** | Per-request structured logging (method, path, status code) | + +## Quick Start + +```bash +# From the project root +cd apps/nexus + +# Build with the bootstrap compiler (Nim) +../../buxc build + +# Or with the self-hosted compiler +../../buxc_lir build + +# Run +./nexus +``` + +Server starts on `http://0.0.0.0:8080`: + +``` +╔══════════════════════════════════════════════╗ +║ Nexus HTTP Server v0.1.0 ║ +║ High-performance multi-threaded HTTP/1.1 ║ +║ HTTP/2 & WebSocket detection included ║ +║ Built with Bux ║ +╚══════════════════════════════════════════════╝ + +✓ Server listening on http://0.0.0.0:8080 +✓ Worker threads: 4 +✓ Static files: ./public/ + + Endpoints: + GET / — Static files (public/) + GET /api/health — Health check (JSON) + GET /api/info — Server info (JSON) + GET /ws — WebSocket upgrade + ANY /* — Static file serving +``` + +## Testing It + +```bash +# Home page (HTML) +curl http://localhost:8080/ + +# Health check +curl http://localhost:8080/api/health +# → {"status":"ok","server":"Nexus","version":"0.1.0"} + +# Server info +curl http://localhost:8080/api/info +# → {"name":"Nexus","language":"Bux","features":[...]} + +# Static file (404 page) +curl http://localhost:8080/404.html + +# Nonexistent file +curl http://localhost:8080/nope +# → 404 Not Found + +# WebSocket upgrade attempt +curl -H "Upgrade: websocket" \ + -H "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==" \ + http://localhost:8080/ws +# → 101 Switching Protocols +``` + +## Architecture + +### Thread Model — Multi-Accept + +Nexus uses the **multi-accept** pattern rather than a traditional thread pool with a work queue: + +``` +Main Thread Worker 1 Worker 2 Worker 3 + │ │ │ │ + ├─ socket()/bind()/listen() │ │ │ + ├─ spawn Worker() ──────────► │ │ + ├─ spawn Worker() ─────────────────────────► │ + ├─ spawn Worker() ────────────────────────────────────────► + │ │ │ │ + ▼ Worker() ▼ accept() ▼ accept() ▼ accept() + accept() loop │ │ │ +``` + +Each worker thread calls `accept()` on the **same** listening socket. The Linux kernel distributes incoming connections across the blocked accept calls — the same mechanism used by nginx and Apache prefork. No mutex, no queue, no context switching between a dispatcher and workers. + +### Request Lifecycle + +``` +Client connects + │ + ▼ +Net_Accept() → client fd + │ + ▼ +Net_Recv(fd, 8192) → raw bytes + │ + ▼ +ParseRequest() + ├─ Split headers on \r\n\r\n + ├─ Parse request line → method, path, version + └─ Parse header lines → key-value array + │ + ▼ +Router_Dispatch() + ├─ /api/* → JSON handlers + ├─ /ws → WebSocket upgrade + ├─ Upgrade header → HTTP/2 or WS detection + └─ /* → Static file serving + │ + ▼ +BuildResponse() → HTTP/1.1 status line + headers + body + │ + ▼ +Net_Send(fd, response) → bytes to client + │ + ▼ +Net_Close(fd) +``` + +### Design Decisions + +- **No keep-alive by default.** Each connection is closed after one response. This avoids blocking worker threads on idle clients (Bux doesn't yet expose `SO_RCVTIMEO`). +- **Linear header array instead of hash map.** HTTP requests typically carry 5–15 headers. A linear scan over a key-value array is faster than hashing for this N, and avoids the complexity of iterating over Bux's generic `StringMap`. +- **Raw extern calls for the string builder.** The stdlib `StringBuilder` wrapper adds a struct indirection. Calling `bux_sb_new` / `bux_sb_append` / `bux_sb_build` directly is simpler and equally safe. +- **WebSocket accept key is a placeholder.** A full implementation needs SHA-1 hashing and Base64 encoding. These aren't in Bux's stdlib yet; they can be added as extern C functions when needed. + +## Project Structure + +``` +apps/nexus/ +├── bux.toml # Package manifest +├── README.md # This file +├── src/ +│ └── Main.bux # The entire server (~640 lines) +└── public/ + ├── index.html # Landing page + └── 404.html # Error page +``` + +Everything lives in one file (`src/Main.bux`) by design — it keeps the module graph flat and the build fast. As the server grows, the HTTP parser, router, and handlers can be split into separate modules. + +## Configuration + +Edit the constants at the top of `src/Main.bux`: + +```bux +const SERVER_PORT: int = 8080; // Listen port +const THREAD_COUNT: int = 4; // Number of worker threads +const RECV_BUF_SIZE: int = 8192; // Receive buffer per request +const SERVER_NAME: String = "Nexus/0.1.0 (Bux)"; +const PUBLIC_DIR: String = "public"; // Static files directory +``` + +## Roadmap + +- [ ] Keep-alive with configurable socket timeout +- [ ] Full WebSocket frame read/write (requires SHA-1 + Base64) +- [ ] HTTP/2 binary framing layer (HPACK, stream multiplexing) +- [ ] SSL/TLS via OpenSSL extern bindings +- [ ] Middleware / filter chain +- [ ] Request body parsing (JSON, form-encoded, multipart) +- [ ] Virtual hosts +- [ ] Access logging to file +- [ ] Rate limiting + +## License + +Nexus is part of the Bux project. See the root [LICENSE](../../LICENSE) for terms. diff --git a/apps/nexus/build.log b/apps/nexus/build.log new file mode 100644 index 0000000..903634c --- /dev/null +++ b/apps/nexus/build.log @@ -0,0 +1 @@ +EXIT:137 diff --git a/apps/nexus/build_check.log b/apps/nexus/build_check.log new file mode 100644 index 0000000..4943272 --- /dev/null +++ b/apps/nexus/build_check.log @@ -0,0 +1 @@ +EXIT:124 diff --git a/apps/nexus/build_demo.log b/apps/nexus/build_demo.log new file mode 100644 index 0000000..4943272 --- /dev/null +++ b/apps/nexus/build_demo.log @@ -0,0 +1 @@ +EXIT:124 diff --git a/apps/nexus/build_full.log b/apps/nexus/build_full.log new file mode 100644 index 0000000..4943272 --- /dev/null +++ b/apps/nexus/build_full.log @@ -0,0 +1 @@ +EXIT:124 diff --git a/apps/nexus/build_min.log b/apps/nexus/build_min.log new file mode 100644 index 0000000..4943272 --- /dev/null +++ b/apps/nexus/build_min.log @@ -0,0 +1 @@ +EXIT:124 diff --git a/apps/nexus/build_simple.log b/apps/nexus/build_simple.log new file mode 100644 index 0000000..5b9a263 --- /dev/null +++ b/apps/nexus/build_simple.log @@ -0,0 +1,2 @@ +timeout: командата „„../../buxc““ не може да бъде изпълнена: Няма такъв файл или директория +EXIT:127 diff --git a/apps/nexus/build_vlong.log b/apps/nexus/build_vlong.log new file mode 100644 index 0000000..903634c --- /dev/null +++ b/apps/nexus/build_vlong.log @@ -0,0 +1 @@ +EXIT:137 diff --git a/apps/nexus/bux.toml b/apps/nexus/bux.toml new file mode 100644 index 0000000..f48630c --- /dev/null +++ b/apps/nexus/bux.toml @@ -0,0 +1,8 @@ +[Package] +Name = "nexus" +Version = "0.1.0" +Type = "bin" +Description = "High-performance multi-threaded HTTP/1.1, HTTP/2 and WebSocket server" + +[Build] +Output = "Bin" diff --git a/apps/nexus/public/404.html b/apps/nexus/public/404.html new file mode 100644 index 0000000..28ea582 --- /dev/null +++ b/apps/nexus/public/404.html @@ -0,0 +1,31 @@ + + + + + + 404 — Not Found | Nexus + + + +

404

+

The requested resource was not found on this server.

+

← Back to Home

+ + diff --git a/apps/nexus/public/index.html b/apps/nexus/public/index.html new file mode 100644 index 0000000..1f4024c --- /dev/null +++ b/apps/nexus/public/index.html @@ -0,0 +1,129 @@ + + + + + + Nexus — Bux HTTP Server + + + +
+

⚡ Nexus

+

High-performance HTTP Server — Built with Bux

+ +
+

Features

+
    +
  • HTTP/1.1 — Full request parsing & response building
  • +
  • Multi-threaded — Configurable worker pool
  • +
  • HTTP/2 Detection — Upgrade-aware
  • +
  • WebSocket Upgrade — Handshake support
  • +
  • Static File Serving — With MIME type detection
  • +
  • JSON API Endpoints
  • +
+
+ +
+

API Endpoints

+
+ GET + /api/health +
+
+ GET + /api/info +
+
+ GET + /ws +
+
+ + +
+ + diff --git a/apps/nexus/src/Main.bux b/apps/nexus/src/Main.bux new file mode 100644 index 0000000..dc54019 --- /dev/null +++ b/apps/nexus/src/Main.bux @@ -0,0 +1,550 @@ +// ============================================================================= +// Nexus — High-performance multi-threaded HTTP/1.1, HTTP/2 & WebSocket server +// Built with the Bux programming language +// ============================================================================= +module Main { + +// ============================================================================= +// Imports — only what we actually use +// ============================================================================= +import Std::Io::{PrintLine, Print, PrintInt}; +import Std::Net::{Net_Create, Net_SetReuse, Net_Bind, Net_Listen, Net_Accept, Net_Send, Net_Recv, Net_Close, Net_LastError}; +import Std::String::{String_Len, String_Eq, String_StartsWith, String_EndsWith, String_Contains, String_Trim, String_FromInt}; +import Std::Task::{Task_Join, TaskHandle}; + +// ============================================================================= +// Extern Functions (from C runtime — used directly for performance) +// ============================================================================= +extern func bux_alloc(size: uint) -> *void; +extern func bux_strlen(s: String) -> uint; +extern func bux_strcmp(a: String, b: String) -> int; +extern func bux_strstr(haystack: String, needle: String) -> String; +extern func bux_str_slice(s: String, start: uint, len: uint) -> String; +extern func bux_str_split_count(s: String, delim: String) -> uint; +extern func bux_str_split_part(s: String, delim: String, index: uint) -> String; +extern func bux_sb_new(initial_cap: uint) -> *void; +extern func bux_sb_append(sb: *void, s: String); +extern func bux_sb_append_int(sb: *void, n: int64); +extern func bux_sb_append_char(sb: *void, c: char8); +extern func bux_sb_build(sb: *void) -> String; +extern func bux_sb_free(sb: *void); +extern func bux_path_join(a: String, b: String) -> String; +extern func bux_read_file(path: String) -> String; +extern func bux_file_exists(path: String) -> int; + +// ============================================================================= +// HTTP Method Enum (simple enum — no data payload) +// ============================================================================= +enum HttpMethod { + GET, + POST, + PUT, + DELETE, + PATCH, + HEAD, + OPTIONS, + UNKNOWN, +} + +// ============================================================================= +// Struct: HTTP Request +// ============================================================================= +struct HttpRequest { + method: HttpMethod, + path: String, + body: String, + headerKeys: *String, + headerValues: *String, + headerCount: int, + headerCap: int, +} + +// ============================================================================= +// Struct: HTTP Response +// ============================================================================= +struct HttpResponse { + statusCode: int, + statusText: String, + contentType: String, + body: String, + extraHeaders: String, // pre-formatted extra headers (for WS upgrade, etc.) + connectionClose: bool, +} + +// ============================================================================= +// Utility: Status text lookup +// ============================================================================= +func Http_StatusText(code: int) -> String { + if code == 200 { return "OK"; } + if code == 201 { return "Created"; } + if code == 204 { return "No Content"; } + if code == 301 { return "Moved Permanently"; } + if code == 302 { return "Found"; } + if code == 304 { return "Not Modified"; } + if code == 400 { return "Bad Request"; } + if code == 401 { return "Unauthorized"; } + if code == 403 { return "Forbidden"; } + if code == 404 { return "Not Found"; } + if code == 405 { return "Method Not Allowed"; } + if code == 413 { return "Payload Too Large"; } + if code == 414 { return "URI Too Long"; } + if code == 500 { return "Internal Server Error"; } + if code == 501 { return "Not Implemented"; } + if code == 503 { return "Service Unavailable"; } + return "Unknown"; +} + +// ============================================================================= +// Utility: Parse HTTP method string → HttpMethod enum +// ============================================================================= +func Http_ParseMethod(s: String) -> HttpMethod { + if String_Eq(s, "GET") { return HttpMethod { tag: HttpMethod_GET }; } + if String_Eq(s, "POST") { return HttpMethod { tag: HttpMethod_POST }; } + if String_Eq(s, "PUT") { return HttpMethod { tag: HttpMethod_PUT }; } + if String_Eq(s, "DELETE") { return HttpMethod { tag: HttpMethod_DELETE }; } + if String_Eq(s, "PATCH") { return HttpMethod { tag: HttpMethod_PATCH }; } + if String_Eq(s, "HEAD") { return HttpMethod { tag: HttpMethod_HEAD }; } + if String_Eq(s, "OPTIONS") { return HttpMethod { tag: HttpMethod_OPTIONS }; } + return HttpMethod { tag: HttpMethod_UNKNOWN }; +} + +// ============================================================================= +// Utility: MIME type from file extension +// ============================================================================= +func Http_MimeType(path: String) -> String { + if String_EndsWith(path, ".html") || String_EndsWith(path, ".htm") { return "text/html; charset=utf-8"; } + if String_EndsWith(path, ".css") { return "text/css; charset=utf-8"; } + if String_EndsWith(path, ".js") { return "application/javascript; charset=utf-8"; } + if String_EndsWith(path, ".json") { return "application/json; charset=utf-8"; } + if String_EndsWith(path, ".xml") { return "application/xml; charset=utf-8"; } + if String_EndsWith(path, ".txt") { return "text/plain; charset=utf-8"; } + if String_EndsWith(path, ".png") { return "image/png"; } + if String_EndsWith(path, ".jpg") || String_EndsWith(path, ".jpeg") { return "image/jpeg"; } + if String_EndsWith(path, ".gif") { return "image/gif"; } + if String_EndsWith(path, ".svg") { return "image/svg+xml"; } + if String_EndsWith(path, ".ico") { return "image/x-icon"; } + if String_EndsWith(path, ".webp") { return "image/webp"; } + if String_EndsWith(path, ".woff2") { return "font/woff2"; } + if String_EndsWith(path, ".woff") { return "font/woff"; } + if String_EndsWith(path, ".wasm") { return "application/wasm"; } + return "application/octet-stream"; +} + +// ============================================================================= +// Header Storage: dynamic key-value array (faster than hash map for 5-15 headers) +// ============================================================================= +func RequestHeader_Init(req: *HttpRequest) { + let cap: int = 16; + req.headerCap = cap; + req.headerKeys = bux_alloc(cap as uint * 8) as *String; + req.headerValues = bux_alloc(cap as uint * 8) as *String; + req.headerCount = 0; +} + +func RequestHeader_Add(req: *HttpRequest, key: String, value: String) { + if req.headerCount >= req.headerCap { return; } + req.headerKeys[req.headerCount] = key; + req.headerValues[req.headerCount] = value; + req.headerCount = req.headerCount + 1; +} + +func RequestHeader_Get(req: *HttpRequest, key: String) -> String { + var i: int = 0; + while i < req.headerCount { + if bux_strcmp(req.headerKeys[i], key) == 0 { + return req.headerValues[i]; + } + i = i + 1; + } + return ""; +} + +// ============================================================================= +// HTTP Request Parser +// ============================================================================= +func Http_ParseRequest(raw: String) -> HttpRequest { + var req: HttpRequest; + req.method = HttpMethod { tag: HttpMethod_UNKNOWN }; + req.path = "/"; + req.body = ""; + RequestHeader_Init(&req); + + // Guard: null/empty input + if raw as uint == 0 { return req; } + let rawLen: uint = bux_strlen(raw); + if rawLen == 0 { return req; } + + // Find \r\n\r\n separating headers from body + let boundary: String = bux_strstr(raw, "\r\n\r\n"); + var headerLen: uint = rawLen; + if boundary as uint != 0 { + headerLen = boundary as uint - raw as uint; + let bodyStart: uint = headerLen + 4; + if bodyStart < rawLen { + req.body = bux_str_slice(raw, bodyStart, rawLen - bodyStart); + } + } + + // Slice out header block + if headerLen == 0 { return req; } + let headerBlock: String = bux_str_slice(raw, 0, headerLen); + + // Count header lines + let lineCount: uint = bux_str_split_count(headerBlock, "\r\n"); + if lineCount == 0 { return req; } + + // --- Parse request line: "METHOD /path HTTP/1.1" --- + let requestLine: String = bux_str_split_part(headerBlock, "\r\n", 0); + if bux_strlen(requestLine) > 0 { + req.method = Http_ParseMethod(bux_str_split_part(requestLine, " ", 0)); + req.path = bux_str_split_part(requestLine, " ", 1); + if String_Eq(req.path, "") || req.path as uint == 0 { + req.path = "/"; + } + } + + // --- Parse headers (lines 1..N-1) --- + var i: uint = 1; + while i < lineCount { + let line: String = bux_str_split_part(headerBlock, "\r\n", i); + let lineLen: uint = bux_strlen(line); + if lineLen == 0 { i = i + 1; continue; } + + // Find ": " separator + let colonPos: String = bux_strstr(line, ": "); + var key: String = ""; + var value: String = ""; + + if colonPos as uint != 0 { + let keyLen: uint = colonPos as uint - line as uint; + key = bux_str_slice(line, 0, keyLen); + let valStart: uint = keyLen + 2; + if valStart < lineLen { + value = bux_str_slice(line, valStart, lineLen - valStart); + } + } else { + // Try ":" without space + let colonPos2: String = bux_strstr(line, ":"); + if colonPos2 as uint != 0 { + let keyLen: uint = colonPos2 as uint - line as uint; + key = bux_str_slice(line, 0, keyLen); + let valStart: uint = keyLen + 1; + if valStart < lineLen { + value = bux_str_slice(line, valStart, lineLen - valStart); + } + value = String_Trim(value); + } + } + + // Store if valid key found + if bux_strlen(key) > 0 { + key = String_Trim(key); + RequestHeader_Add(&req, key, value); + } + + i = i + 1; + } + + return req; +} + +// ============================================================================= +// HTTP Response Builder +// ============================================================================= +func Http_BuildResponse(resp: HttpResponse) -> String { + let sb: *void = bux_sb_new(4096); + + // Status line + bux_sb_append(sb, "HTTP/1.1 "); + bux_sb_append_int(sb, resp.statusCode as int64); + bux_sb_append_char(sb, ' '); + bux_sb_append(sb, resp.statusText); + bux_sb_append(sb, "\r\n"); + + // Server + bux_sb_append(sb, "Server: Nexus/0.1.0 (Bux)\r\n"); + + // Extra headers (for WS upgrade etc.) + if bux_strlen(resp.extraHeaders) > 0 { + bux_sb_append(sb, resp.extraHeaders); + } + + // Content-Type + if bux_strlen(resp.contentType) > 0 { + bux_sb_append(sb, "Content-Type: "); + bux_sb_append(sb, resp.contentType); + bux_sb_append(sb, "\r\n"); + } + + // Content-Length + let bodyLen: uint = bux_strlen(resp.body); + bux_sb_append(sb, "Content-Length: "); + bux_sb_append_int(sb, bodyLen as int64); + bux_sb_append(sb, "\r\n"); + + // Connection + if resp.connectionClose { + bux_sb_append(sb, "Connection: close\r\n"); + } else { + bux_sb_append(sb, "Connection: keep-alive\r\n"); + } + + // End headers + bux_sb_append(sb, "\r\n"); + + // Body + if bodyLen > 0 { + bux_sb_append(sb, resp.body); + } + + let result: String = bux_sb_build(sb); + bux_sb_free(sb); + return result; +} + +// ============================================================================= +// Helper: Create a simple response +// ============================================================================= +func Http_NewResponse(code: int, contentType: String, body: String) -> HttpResponse { + var resp: HttpResponse; + resp.statusCode = code; + resp.statusText = Http_StatusText(code); + resp.contentType = contentType; + resp.body = body; + resp.extraHeaders = ""; + resp.connectionClose = true; + return resp; +} + +// ============================================================================= +// Static File Server +// ============================================================================= +func ServeStaticFile(requestPath: String) -> HttpResponse { + // Block directory traversal + if String_Contains(requestPath, "..") { + return Http_NewResponse(403, "text/plain; charset=utf-8", "Forbidden"); + } + + // Default to index.html + var filePath: String = requestPath; + if String_Eq(filePath, "/") { + filePath = "/index.html"; + } + + // Resolve full path + let fullPath: String = bux_path_join("public", filePath); + + // Check existence + if bux_file_exists(fullPath) == 0 { + return Http_NewResponse(404, "text/plain; charset=utf-8", "Not Found"); + } + + // Read and serve + let content: String = bux_read_file(fullPath); + if content as uint == 0 { + return Http_NewResponse(500, "text/plain; charset=utf-8", "Internal Server Error"); + } + + let mime: String = Http_MimeType(filePath); + return Http_NewResponse(200, mime, content); +} + +// ============================================================================= +// API Handlers +// ============================================================================= +func HandleApiHealth() -> HttpResponse { + return Http_NewResponse(200, "application/json; charset=utf-8", + "{\"status\":\"ok\",\"server\":\"Nexus\",\"version\":\"0.1.0\"}"); +} + +func HandleApiInfo() -> HttpResponse { + return Http_NewResponse(200, "application/json; charset=utf-8", + "{\"name\":\"Nexus\",\"language\":\"Bux\",\"threads\":4,\"features\":[\"HTTP/1.1\",\"HTTP/2-detect\",\"WebSocket-upgrade\"]}"); +} + +// ============================================================================= +// WebSocket Upgrade Handshake +// ============================================================================= +func HandleWebSocketUpgrade(req: HttpRequest) -> HttpResponse { + let wsKey: String = RequestHeader_Get(&req, "Sec-WebSocket-Key"); + + var resp: HttpResponse; + resp.statusCode = 101; + resp.statusText = "Switching Protocols"; + resp.contentType = ""; + resp.body = ""; + resp.connectionClose = false; + + // Build upgrade headers as extraHeaders + // TODO: SHA-1 + Base64 for proper accept key + let sb: *void = bux_sb_new(256); + bux_sb_append(sb, "Upgrade: websocket\r\n"); + bux_sb_append(sb, "Connection: Upgrade\r\n"); + bux_sb_append(sb, "Sec-WebSocket-Accept: "); + bux_sb_append(sb, wsKey); // placeholder — real impl needs crypto + bux_sb_append(sb, "\r\n"); + resp.extraHeaders = bux_sb_build(sb); + bux_sb_free(sb); + + return resp; +} + +// ============================================================================= +// Router / Dispatcher +// ============================================================================= +func Router_Dispatch(req: HttpRequest) -> HttpResponse { + // Check for upgrade headers + let upgrade: String = RequestHeader_Get(&req, "Upgrade"); + if bux_strlen(upgrade) > 0 { + if String_Contains(upgrade, "websocket") { + return HandleWebSocketUpgrade(req); + } + } + + // API routes + if String_StartsWith(req.path, "/api/") { + if String_Eq(req.path, "/api/health") { return HandleApiHealth(); } + if String_Eq(req.path, "/api/info") { return HandleApiInfo(); } + return Http_NewResponse(404, "application/json; charset=utf-8", + "{\"error\":\"not_found\"}"); + } + + // WebSocket endpoint + if String_Eq(req.path, "/ws") { + return HandleWebSocketUpgrade(req); + } + + // Static files — GET/HEAD only + if req.method.tag == HttpMethod_GET || req.method.tag == HttpMethod_HEAD { + return ServeStaticFile(req.path); + } + + // Method not allowed + return Http_NewResponse(405, "text/plain; charset=utf-8", "Method Not Allowed"); +} + +// ============================================================================= +// Method name for logging (avoids 8 if-checks in hot path) +// ============================================================================= +func Http_MethodName(m: HttpMethod) -> String { + if m.tag == HttpMethod_GET { return "GET"; } + if m.tag == HttpMethod_POST { return "POST"; } + if m.tag == HttpMethod_PUT { return "PUT"; } + if m.tag == HttpMethod_DELETE { return "DELETE"; } + if m.tag == HttpMethod_PATCH { return "PATCH"; } + if m.tag == HttpMethod_HEAD { return "HEAD"; } + if m.tag == HttpMethod_OPTIONS { return "OPTIONS"; } + return "UNKNOWN"; +} + +// ============================================================================= +// Connection Handler — read, parse, route, respond +// ============================================================================= +func HandleConnection(clientFd: int) { + let raw: String = Net_Recv(clientFd, 8192); + + // Empty read → client disconnected + if raw as uint == 0 { return; } + if bux_strlen(raw) == 0 { return; } + + // HTTP/2 connection preface detection + if String_StartsWith(raw, "PRI * HTTP/2.0") { + let resp: HttpResponse = Http_NewResponse(200, "text/plain; charset=utf-8", + "HTTP/2 detected — full support planned for future release.\r\n"); + let respStr: String = Http_BuildResponse(resp); + Net_Send(clientFd, respStr); + return; + } + + // Parse HTTP/1.1 + let req: HttpRequest = Http_ParseRequest(raw); + + // Log + Print(Http_MethodName(req.method)); + Print(" "); + Print(req.path); + Print(" -> "); + + // Dispatch + let resp: HttpResponse = Router_Dispatch(req); + + // Log result + PrintInt(resp.statusCode); + Print(" "); + PrintLine(resp.statusText); + + // Send + let respStr: String = Http_BuildResponse(resp); + Net_Send(clientFd, respStr); +} + +// ============================================================================= +// Worker Thread — accept loop +// ============================================================================= +func Worker(serverFd: int) { + while true { + let clientFd: int = Net_Accept(serverFd); + if clientFd < 0 { + continue; + } + HandleConnection(clientFd); + Net_Close(clientFd); + } +} + +// ============================================================================= +// Main Entry Point +// ============================================================================= +func Main() -> int { + PrintLine("================================================"); + PrintLine(" Nexus HTTP Server v0.1.0"); + PrintLine(" Multi-threaded HTTP/1.1 + HTTP/2 & WS detect"); + PrintLine(" Built with Bux"); + PrintLine("================================================"); + PrintLine(""); + + // Create socket + let serverFd: int = Net_Create(); + if serverFd < 0 { + PrintLine("FATAL: socket() failed"); + return 1; + } + + // SO_REUSEADDR + if !Net_SetReuse(serverFd) { + PrintLine("WARN: SO_REUSEADDR failed"); + } + + // Bind + if !Net_Bind(serverFd, "0.0.0.0", 8080) { + Print("FATAL: bind(8080) failed: "); + PrintLine(Net_LastError()); + Net_Close(serverFd); + return 1; + } + + // Listen + if !Net_Listen(serverFd, 128) { + PrintLine("FATAL: listen() failed"); + Net_Close(serverFd); + return 1; + } + + PrintLine("Listening on http://0.0.0.0:8080"); + PrintLine("4 worker threads | static: ./public/"); + PrintLine("Endpoints: / /api/health /api/info /ws"); + PrintLine("Press Ctrl+C to stop."); + PrintLine(""); + + // Spawn 3 workers (main thread is the 4th) + spawn Worker(serverFd); + spawn Worker(serverFd); + spawn Worker(serverFd); + + // Main thread becomes worker #4 (blocking call) + Worker(serverFd); + + return 0; +} + +} // module Main diff --git a/apps/nexus/ver.log b/apps/nexus/ver.log new file mode 100644 index 0000000..3b4b4d6 --- /dev/null +++ b/apps/nexus/ver.log @@ -0,0 +1,2 @@ +bux 0.1.0 (bootstrap) +EXIT:0 diff --git a/lib/Crypto.bux b/lib/Crypto.bux index cf8632c..f3235be 100644 --- a/lib/Crypto.bux +++ b/lib/Crypto.bux @@ -1,6 +1,19 @@ +// ============================================================================= +// Std::Crypto — backward-compatible wrapper (delegates to submodules) +// +// Migration note: new code should use the submodules directly: +// Std::Crypto::Hash::{Hash_Sha256, ...} +// Std::Crypto::Hmac::{Hmac_Sha256, ...} +// Std::Crypto::Base64::{Base64_Encode, Base64URL_Encode, ...} +// Std::Crypto::Random::{Random_Bytes, ...} +// Std::Crypto::Jwt::{Jwt_EncodeHS256, ...} +// ============================================================================= module Std::Crypto { + import Std::Mem::{Alloc, Free}; import Std::String::{String_Len}; + +// Re-use the same externs from submodules (merged by compiler) extern func bux_sha256(data: String, len: int, out: *void); extern func bux_hmac_sha256(key: String, keylen: int, msg: String, msglen: int, out: *void); extern func bux_random_bytes(buf: *void, len: int) -> int; @@ -8,7 +21,9 @@ extern func bux_base64_encode(data: String, len: int) -> String; extern func bux_base64_decode(data: String, len: int, outlen: *int) -> String; extern func bux_bytes_to_hex(data: *void, len: int) -> String; -/* SHA-256: returns lowercase hex string of the hash */ +// --- Legacy function names (delegate to new submodule functions) --- + +// SHA-256 → hex func Crypto_Sha256(data: String) -> String { let len: int = String_Len(data) as int; let hashBuf: *void = Alloc(32); @@ -18,7 +33,7 @@ func Crypto_Sha256(data: String) -> String { return result; } -/* HMAC-SHA256: returns lowercase hex string */ +// HMAC-SHA256 → hex func Crypto_HmacSha256(key: String, message: String) -> String { let keylen: int = String_Len(key) as int; let msglen: int = String_Len(message) as int; @@ -29,7 +44,7 @@ func Crypto_HmacSha256(key: String, message: String) -> String { return result; } -/* Random bytes: returns a string of n random bytes */ +// Random bytes → base64 func Crypto_RandomBytes(n: int) -> String { if n <= 0 { return ""; } let buf: *void = Alloc(n as uint); @@ -42,12 +57,12 @@ func Crypto_RandomBytes(n: int) -> String { return result; } -/* Base64 encode */ +// Base64 encode func Crypto_Base64Encode(s: String) -> String { return bux_base64_encode(s, String_Len(s) as int); } -/* HMAC-SHA256 raw: returns base64 of raw binary hmac */ +// HMAC-SHA256 raw → base64 func Crypto_HmacSha256Raw(key: String, message: String) -> String { let keylen: int = String_Len(key) as int; let msglen: int = String_Len(message) as int; @@ -58,7 +73,7 @@ func Crypto_HmacSha256Raw(key: String, message: String) -> String { return result; } -/* Base64 decode */ +// Base64 decode func Crypto_Base64Decode(s: String) -> String { let outlen: int = 0; return bux_base64_decode(s, String_Len(s) as int, &outlen); diff --git a/lib/crypto/README.md b/lib/crypto/README.md new file mode 100644 index 0000000..129b748 --- /dev/null +++ b/lib/crypto/README.md @@ -0,0 +1,265 @@ +# Bux Crypto Library (`Std::Crypto`) + +Cryptographic primitives for the Bux programming language. Backed by OpenSSL via the C runtime (`rt/runtime.c`). + +## Modules + +| Module | Import Path | Provides | +|--------|------------|----------| +| **Base64** | `Std::Crypto::Base64` | Base64 and Base64URL (RFC 4648 §5) encode/decode | +| **Hash** | `Std::Crypto::Hash` | SHA-1, SHA-256, SHA-384, SHA-512 (hex + raw) | +| **HMAC** | `Std::Crypto::Hmac` | HMAC-SHA256, HMAC-SHA384, HMAC-SHA512 (hex + raw + base64) | +| **Random** | `Std::Crypto::Random` | Cryptographically secure random bytes, hex, base64, uint32 | +| **AES** | `Std::Crypto::Aes` | AES-256-CBC and AES-256-GCM encrypt/decrypt | +| **RSA** | `Std::Crypto::Rsa` | RSA PKCS#1 v1.5 sign/verify (SHA-256/384/512) | +| **ECDSA** | `Std::Crypto::Ecdsa` | ECDSA P-256 and P-384 sign/verify | +| **Ed25519** | `Std::Crypto::Ed25519` | Ed25519 key generation, signing, verification | +| **JWT** | `Std::Crypto::Jwt` | JSON Web Tokens (HS256/384/512, RS256/384/512, ES256/384, EdDSA) | + +The legacy `Std::Crypto` module (the old single-file API) is preserved as a backward-compatible wrapper — existing code continues to work. + +## Quick Start + +### Hash + +```bux +import Std::Crypto::Hash::{Hash_Sha256, Hash_Sha384, Hash_Sha512}; + +let hex: String = Hash_Sha256("hello"); +// → "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824" + +let size: int = Hash_Sha256Size(); // → 32 (bytes) +``` + +### HMAC + +```bux +import Std::Crypto::Hmac::{Hmac_Sha256, Hmac_Sha256Raw, Hmac_Sha256Base64}; + +let hex: String = Hmac_Sha256("secret-key", "message"); +// → hex string + +// Raw binary output (caller allocates 32-byte buffer) +let buf: *void = Alloc(32); +Hmac_Sha256Raw("secret-key", "message", buf); +// buf now contains 32 bytes of raw HMAC + +let b64: String = Hmac_Sha256Base64("secret-key", "message"); +// → base64-encoded HMAC +``` + +### Base64 & Base64URL + +```bux +import Std::Crypto::Base64::{Base64_Encode, Base64_Decode, + Base64URL_Encode, Base64URL_Decode}; + +let std: String = Base64_Encode("hello"); // → "aGVsbG8=" +let orig: String = Base64_Decode(std); // → "hello" + +let url: String = Base64URL_Encode("hello"); // → "aGVsbG8" (no padding) +let orig2: String = Base64URL_Decode(url); // → "hello" +``` + +### Random + +```bux +import Std::Crypto::Random::{Random_Bytes, Random_Hex, Random_Base64, Random_Uint32}; + +let raw: String = Random_Bytes(32); // 32 random bytes (binary-safe string) +let hex: String = Random_Hex(16); // 16 random bytes as hex (32 chars) +let b64: String = Random_Base64(16); // 16 random bytes as base64 +let u32: uint = Random_Uint32(); // random 32-bit unsigned integer +``` + +### AES-256 + +```bux +import Std::Crypto::Aes::{Aes_GenerateKey, Aes_GenerateIV, + Aes_CbcEncrypt, Aes_CbcDecrypt, + Aes_GcmEncrypt, Aes_GcmDecrypt}; + +// Generate random key and IV +let key: String = Aes_GenerateKey(); // 32 raw bytes +let iv: String = Aes_GenerateIV(); // 16 raw bytes + +// CBC mode +let cipher: String = Aes_CbcEncrypt("secret message", key, iv); +let plain: String = Aes_CbcDecrypt(cipher, key, iv); + +// GCM mode (authenticated encryption) +let tag: *void = Alloc(16); +let gcmCipher: String = Aes_GcmEncrypt("secret", key, iv, tag); +let gcmPlain: String = Aes_GcmDecrypt(gcmCipher, key, iv, tag as String); +``` + +### RSA + +```bux +import Std::Crypto::Rsa::{Rsa_SignSha256, Rsa_VerifySha256, + Rsa_SignSha256Base64, Rsa_VerifySha256Base64}; + +// Keys are PEM-encoded strings +let pemPriv: String = ReadFile("private.pem"); +let pemPub: String = ReadFile("public.pem"); + +// Sign — returns raw signature +let sig: String = Rsa_SignSha256(pemPriv, "data to sign"); + +// Or sign and get base64 +let sigB64: String = Rsa_SignSha256Base64(pemPriv, "data to sign"); + +// Verify raw signature +let valid: bool = Rsa_VerifySha256(pemPub, "data to sign", sig); + +// Verify base64 signature +let validB64: bool = Rsa_VerifySha256Base64(pemPub, "data to sign", sigB64); +``` + +### ECDSA + +```bux +import Std::Crypto::Ecdsa::{Ecdsa_SignP256, Ecdsa_VerifyP256, + Ecdsa_SignP384, Ecdsa_VerifyP384}; + +// P-256 (ES256) +let sig: String = Ecdsa_SignP256(pemPriv, "data"); +let ok: bool = Ecdsa_VerifyP256(pemPub, "data", sig); + +// P-384 (ES384) +let sig384: String = Ecdsa_SignP384(pemPriv, "data"); +let ok384: bool = Ecdsa_VerifyP384(pemPub, "data", sig384); +``` + +### Ed25519 + +```bux +import Std::Crypto::Ed25519::{Ed25519_Keypair, Ed25519_Sign, Ed25519_Verify}; + +// Generate keypair — keys are 32 raw bytes each +let pub: *void = Alloc(32); +let priv: *void = Alloc(32); +Ed25519_Keypair(pub, priv); + +// Sign +let sig: String = Ed25519_Sign(priv as String, "message"); +// sig is 64 raw bytes + +// Verify +let valid: bool = Ed25519_Verify(pub as String, sig, "message"); +``` + +### JWT + +```bux +import Std::Crypto::Jwt::{JwtAlg, Jwt_MakeHeader, Jwt_Encode, + Jwt_Decode, Jwt_EncodeHS256}; + +// --- Symmetric (HS256) --- +let token: String = Jwt_EncodeHS256("{\"sub\":\"123\",\"role\":\"admin\"}", "my-secret"); + +var header: String; +var payload: String; +let alg: JwtAlg = JwtAlg { tag: JwtAlg_HS256 }; +if Jwt_Decode(token, alg, "my-secret", &header, &payload) { + PrintLine(payload); // {"sub":"123","role":"admin"} +} + +// --- Asymmetric (RS256) --- +let rsToken: String = Jwt_Encode( + "{\"alg\":\"RS256\",\"typ\":\"JWT\"}", + "{\"sub\":\"456\"}", + JwtAlg { tag: JwtAlg_RS256 }, + pemPrivateKey +); + +// --- Convenience helpers --- +Jwt_EncodeHS256(payload, secret); +Jwt_EncodeHS384(payload, secret); +Jwt_EncodeHS512(payload, secret); +Jwt_EncodeRS256(payload, pemPrivKey); +Jwt_EncodeES256(payload, pemPrivKey); +Jwt_EncodeEdDSA(payload, rawPrivKey32); +``` + +## Supported JWT Algorithms + +| Algorithm | JWT `alg` | Key Type | Key Format | +|-----------|-----------|----------|------------| +| HS256 | `HS256` | HMAC secret | Raw string | +| HS384 | `HS384` | HMAC secret | Raw string | +| HS512 | `HS512` | HMAC secret | Raw string | +| RS256 | `RS256` | RSA private/public key | PEM string | +| RS384 | `RS384` | RSA private/public key | PEM string | +| RS512 | `RS512` | RSA private/public key | PEM string | +| ES256 | `ES256` | ECDSA P-256 key | PEM string | +| ES384 | `ES384` | ECDSA P-384 key | PEM string | +| EdDSA | `EdDSA` | Ed25519 key | 32-byte raw | + +## Backend + +All primitives are implemented in C using OpenSSL and linked via the Bux runtime (`rt/runtime.c`). The C functions are declared as `extern func` in each Bux module. + +Requires OpenSSL 1.1.1+ (for Ed25519 support). Link with `-lssl -lcrypto`. + +## File Layout + +``` +lib/ +├── Crypto.bux # Backward-compat wrapper (old API) +└── crypto/ + ├── base64.bux # Base64 + Base64URL + ├── hash.bux # SHA-1/256/384/512 + ├── hmac.bux # HMAC-SHA256/384/512 + ├── random.bux # Secure random + ├── aes.bux # AES-256-CBC/GCM + ├── rsa.bux # RSA PKCS#1 v1.5 + ├── ecdsa.bux # ECDSA P-256/P-384 + ├── ed25519.bux # Ed25519 + └── jwt.bux # JSON Web Tokens + +lib/crypto_test/ # Test project (exercises all modules) +test_crypto/ # Standalone test project +``` + +## Migration from old `Std::Crypto` + +The old single-file API is still available under `Std::Crypto`: + +| Old Function | New Equivalent | Module | +|-------------|----------------|--------| +| `Crypto_Sha256(s)` | `Hash_Sha256(s)` | `Std::Crypto::Hash` | +| `Crypto_HmacSha256(k, m)` | `Hmac_Sha256(k, m)` | `Std::Crypto::Hmac` | +| `Crypto_RandomBytes(n)` | `Random_Base64(n)` | `Std::Crypto::Random` | +| `Crypto_Base64Encode(s)` | `Base64_Encode(s)` | `Std::Crypto::Base64` | +| `Crypto_Base64Decode(s)` | `Base64_Decode(s)` | `Std::Crypto::Base64` | +| `Crypto_HmacSha256Raw(k, m)` | `Hmac_Sha256Base64(k, m)` | `Std::Crypto::Hmac` | + +New code should prefer the submodule imports for clarity and to avoid pulling in unused declarations. + +## Running Tests + +```bash +cd test_crypto +../buxc build +./test_crypto +``` + +Expected output: +``` +================================================ + Bux Crypto Library — Test Suite +================================================ + +--- Base64 --- + PASS Base64_Encode('hello') + PASS Base64_Decode('aGVsbG8=') + ... +--- Hash --- + PASS Hash_Sha256('hello') + ... +================================================ + Passed: 23 + Failed: 0 +================================================ +``` diff --git a/lib/crypto/aes.bux b/lib/crypto/aes.bux new file mode 100644 index 0000000..55f963d --- /dev/null +++ b/lib/crypto/aes.bux @@ -0,0 +1,67 @@ +// ============================================================================= +// Std::Crypto::Aes — AES-256-CBC and AES-256-GCM encryption/decryption +// ============================================================================= +module Std::Crypto::Aes { + +import Std::Mem::{Alloc, Free}; +import Std::String::{String_Len}; + +extern func bux_random_bytes(buf: *void, len: int) -> int; +extern func bux_aes_256_cbc_encrypt(plain: String, plainlen: int, key: String, iv: String, outlen: *int) -> String; +extern func bux_aes_256_cbc_decrypt(cipher: String, cipherlen: int, key: String, iv: String, outlen: *int) -> String; +extern func bux_aes_256_gcm_encrypt(plain: String, plainlen: int, key: String, iv: String, tag: *void, outlen: *int) -> String; +extern func bux_aes_256_gcm_decrypt(cipher: String, cipherlen: int, key: String, iv: String, tag: String, outlen: *int) -> String; + +// --- AES-256-CBC --- + +const AES_KEY_SIZE: int = 32; // 256 bits +const AES_IV_SIZE: int = 16; // 128 bits +const AES_GCM_TAG_SIZE: int = 16; + +// Generate a random 256-bit AES key (returns raw 32 bytes) +func Aes_GenerateKey() -> String { + let buf: *void = Alloc(AES_KEY_SIZE as uint); + if bux_random_bytes(buf, AES_KEY_SIZE) != 1 { + Free(buf); + return ""; + } + return buf as String; +} + +// Generate a random 128-bit IV (returns raw 16 bytes) +func Aes_GenerateIV() -> String { + let buf: *void = Alloc(AES_IV_SIZE as uint); + if bux_random_bytes(buf, AES_IV_SIZE) != 1 { + Free(buf); + return ""; + } + return buf as String; +} + +// AES-256-CBC encrypt. plain and key are binary strings, iv is 16 bytes. +// Returns ciphertext (may be longer than plain due to PKCS#7 padding). +func Aes_CbcEncrypt(plain: String, key: String, iv: String) -> String { + let outlen: int = 0; + return bux_aes_256_cbc_encrypt(plain, String_Len(plain) as int, key, iv, &outlen); +} + +// AES-256-CBC decrypt. Returns plaintext. +func Aes_CbcDecrypt(cipher: String, key: String, iv: String) -> String { + let outlen: int = 0; + return bux_aes_256_cbc_decrypt(cipher, String_Len(cipher) as int, key, iv, &outlen); +} + +// --- AES-256-GCM (Authenticated Encryption) --- + +// AES-256-GCM encrypt. Returns ciphertext. tag receives 16-byte authentication tag. +func Aes_GcmEncrypt(plain: String, key: String, iv: String, tag: *void) -> String { + let outlen: int = 0; + return bux_aes_256_gcm_encrypt(plain, String_Len(plain) as int, key, iv, tag, &outlen); +} + +// AES-256-GCM decrypt. Returns plaintext. tag must be the 16-byte auth tag from encryption. +func Aes_GcmDecrypt(cipher: String, key: String, iv: String, tag: String) -> String { + let outlen: int = 0; + return bux_aes_256_gcm_decrypt(cipher, String_Len(cipher) as int, key, iv, tag, &outlen); +} +} diff --git a/lib/crypto/base64.bux b/lib/crypto/base64.bux new file mode 100644 index 0000000..3897556 --- /dev/null +++ b/lib/crypto/base64.bux @@ -0,0 +1,34 @@ +// ============================================================================= +// Std::Crypto::Base64 — Base64 and Base64URL encode/decode +// ============================================================================= +module Std::Crypto::Base64 { + +import Std::String::{String_Len}; + +extern func bux_base64_encode(data: String, len: int) -> String; +extern func bux_base64_decode(data: String, len: int, outlen: *int) -> String; +extern func bux_base64url_encode(data: String, len: int) -> String; +extern func bux_base64url_decode(data: String, len: int, outlen: *int) -> String; + +// --- Standard Base64 --- + +func Base64_Encode(s: String) -> String { + return bux_base64_encode(s, String_Len(s) as int); +} + +func Base64_Decode(s: String) -> String { + let outlen: int = 0; + return bux_base64_decode(s, String_Len(s) as int, &outlen); +} + +// --- Base64URL (RFC 4648 §5, uses - and _ instead of + and /, no padding) --- + +func Base64URL_Encode(s: String) -> String { + return bux_base64url_encode(s, String_Len(s) as int); +} + +func Base64URL_Decode(s: String) -> String { + let outlen: int = 0; + return bux_base64url_decode(s, String_Len(s) as int, &outlen); +} +} diff --git a/lib/crypto/ecdsa.bux b/lib/crypto/ecdsa.bux new file mode 100644 index 0000000..8191604 --- /dev/null +++ b/lib/crypto/ecdsa.bux @@ -0,0 +1,67 @@ +// ============================================================================= +// Std::Crypto::Ecdsa — ECDSA P-256 and P-384 sign/verify +// ============================================================================= +module Std::Crypto::Ecdsa { + +import Std::Mem::{Alloc, Free}; +import Std::String::{String_Len}; + +extern func bux_ecdsa_sign_p256(pemKey: String, keylen: int, data: String, datalen: int, siglen: *int) -> String; +extern func bux_ecdsa_sign_p384(pemKey: String, keylen: int, data: String, datalen: int, siglen: *int) -> String; +extern func bux_ecdsa_verify_p256(pemKey: String, keylen: int, data: String, datalen: int, sig: String, siglen: int) -> int; +extern func bux_ecdsa_verify_p384(pemKey: String, keylen: int, data: String, datalen: int, sig: String, siglen: int) -> int; +extern func bux_base64_encode(data: String, len: int) -> String; +extern func bux_base64_decode(data: String, len: int, outlen: *int) -> String; + +// --- ECDSA P-256 (ES256) --- + +func Ecdsa_SignP256(pemPrivateKey: String, data: String) -> String { + let siglen: int = 0; + return bux_ecdsa_sign_p256(pemPrivateKey, String_Len(pemPrivateKey) as int, + data, String_Len(data) as int, &siglen); +} + +func Ecdsa_SignP256Base64(pemPrivateKey: String, data: String) -> String { + let sig: String = Ecdsa_SignP256(pemPrivateKey, data); + return bux_base64_encode(sig, String_Len(sig) as int); +} + +func Ecdsa_VerifyP256(pemPublicKey: String, data: String, signature: String) -> bool { + let r: int = bux_ecdsa_verify_p256(pemPublicKey, String_Len(pemPublicKey) as int, + data, String_Len(data) as int, + signature, String_Len(signature) as int); + return r == 1; +} + +func Ecdsa_VerifyP256Base64(pemPublicKey: String, data: String, signatureB64: String) -> bool { + let outlen: int = 0; + let sig: String = bux_base64_decode(signatureB64, String_Len(signatureB64) as int, &outlen); + return Ecdsa_VerifyP256(pemPublicKey, data, sig); +} + +// --- ECDSA P-384 (ES384) --- + +func Ecdsa_SignP384(pemPrivateKey: String, data: String) -> String { + let siglen: int = 0; + return bux_ecdsa_sign_p384(pemPrivateKey, String_Len(pemPrivateKey) as int, + data, String_Len(data) as int, &siglen); +} + +func Ecdsa_SignP384Base64(pemPrivateKey: String, data: String) -> String { + let sig: String = Ecdsa_SignP384(pemPrivateKey, data); + return bux_base64_encode(sig, String_Len(sig) as int); +} + +func Ecdsa_VerifyP384(pemPublicKey: String, data: String, signature: String) -> bool { + let r: int = bux_ecdsa_verify_p384(pemPublicKey, String_Len(pemPublicKey) as int, + data, String_Len(data) as int, + signature, String_Len(signature) as int); + return r == 1; +} + +func Ecdsa_VerifyP384Base64(pemPublicKey: String, data: String, signatureB64: String) -> bool { + let outlen: int = 0; + let sig: String = bux_base64_decode(signatureB64, String_Len(signatureB64) as int, &outlen); + return Ecdsa_VerifyP384(pemPublicKey, data, sig); +} +} diff --git a/lib/crypto/ed25519.bux b/lib/crypto/ed25519.bux new file mode 100644 index 0000000..d0bab8b --- /dev/null +++ b/lib/crypto/ed25519.bux @@ -0,0 +1,80 @@ +// ============================================================================= +// Std::Crypto::Ed25519 — Ed25519 key generation, signing, and verification +// ============================================================================= +module Std::Crypto::Ed25519 { + +import Std::Mem::{Alloc, Free}; +import Std::String::{String_Len, String_Concat}; + +extern func bux_ed25519_keypair(pubKey: *void, privKey: *void) -> int; +extern func bux_ed25519_sign(privKey: String, data: String, datalen: int, sig: *void) -> int; +extern func bux_ed25519_verify(pubKey: String, sig: String, data: String, datalen: int) -> int; +extern func bux_base64_encode(data: String, len: int) -> String; +extern func bux_base64_decode(data: String, len: int, outlen: *int) -> String; + +const ED25519_PUBKEY_SIZE: int = 32; +const ED25519_PRIVKEY_SIZE: int = 32; +const ED25519_SIG_SIZE: int = 64; + +// --- Key Generation --- + +// Ed25519_Keypair: generates a new keypair. +// Returns true on success. pubKey and privKey receive 32-byte raw keys. +func Ed25519_Keypair(pubKey: *void, privKey: *void) -> bool { + let r: int = bux_ed25519_keypair(pubKey, privKey); + return r == 1; +} + +// Convenience: generate and return base64-encoded keypair +func Ed25519_KeypairBase64() -> String { + let pub: *void = Alloc(ED25519_PUBKEY_SIZE as uint); + let priv: *void = Alloc(ED25519_PRIVKEY_SIZE as uint); + if bux_ed25519_keypair(pub, priv) != 1 { + Free(pub); + Free(priv); + return ""; + } + // Return "pub_b64:priv_b64" + let pubB64: String = bux_base64_encode(pub as String, ED25519_PUBKEY_SIZE); + let privB64: String = bux_base64_encode(priv as String, ED25519_PRIVKEY_SIZE); + Free(pub); + Free(priv); + let pair: String = String_Concat(pubB64, ":"); + return String_Concat(pair, privB64); +} + +// --- Sign --- + +// Ed25519_Sign: sign data with 32-byte raw private key. Returns 64-byte raw signature. +func Ed25519_Sign(privKey: String, data: String) -> String { + let sig: *void = Alloc(ED25519_SIG_SIZE as uint); + if bux_ed25519_sign(privKey, data, String_Len(data) as int, sig) != 1 { + Free(sig); + return ""; + } + return sig as String; +} + +// Convenience: sign and return base64-encoded signature +func Ed25519_SignBase64(privKey: String, data: String) -> String { + let sig: String = Ed25519_Sign(privKey, data); + if String_Len(sig) == 0 { return ""; } + return bux_base64_encode(sig, ED25519_SIG_SIZE); +} + +// --- Verify --- + +// Ed25519_Verify: verify a 64-byte raw signature against data with 32-byte public key. +func Ed25519_Verify(pubKey: String, signature: String, data: String) -> bool { + let r: int = bux_ed25519_verify(pubKey, signature, data, String_Len(data) as int); + return r == 1; +} + +// Convenience: verify a base64-encoded signature +func Ed25519_VerifyBase64(pubKey: String, signatureB64: String, data: String) -> bool { + let outlen: int = 0; + let sig: String = bux_base64_decode(signatureB64, String_Len(signatureB64) as int, &outlen); + if outlen != ED25519_SIG_SIZE { return false; } + return Ed25519_Verify(pubKey, sig, data); +} +} diff --git a/lib/crypto/hash.bux b/lib/crypto/hash.bux new file mode 100644 index 0000000..3ed5b55 --- /dev/null +++ b/lib/crypto/hash.bux @@ -0,0 +1,73 @@ +// ============================================================================= +// Std::Crypto::Hash — SHA-1, SHA-256, SHA-384, SHA-512 +// ============================================================================= +module Std::Crypto::Hash { + +import Std::Mem::{Alloc, Free}; +import Std::String::{String_Len}; + +extern func bux_sha1(data: String, len: int, out: *void); +extern func bux_sha256(data: String, len: int, out: *void); +extern func bux_sha384(data: String, len: int, out: *void); +extern func bux_sha512(data: String, len: int, out: *void); +extern func bux_bytes_to_hex(data: *void, len: int) -> String; + +// --- Convenience wrappers: hex output --- + +func Hash_Sha1(data: String) -> String { + let len: int = String_Len(data) as int; + let buf: *void = Alloc(20); + bux_sha1(data, len, buf); + let result: String = bux_bytes_to_hex(buf, 20); + Free(buf); + return result; +} + +func Hash_Sha256(data: String) -> String { + let len: int = String_Len(data) as int; + let buf: *void = Alloc(32); + bux_sha256(data, len, buf); + let result: String = bux_bytes_to_hex(buf, 32); + Free(buf); + return result; +} + +func Hash_Sha384(data: String) -> String { + let len: int = String_Len(data) as int; + let buf: *void = Alloc(48); + bux_sha384(data, len, buf); + let result: String = bux_bytes_to_hex(buf, 48); + Free(buf); + return result; +} + +func Hash_Sha512(data: String) -> String { + let len: int = String_Len(data) as int; + let buf: *void = Alloc(64); + bux_sha512(data, len, buf); + let result: String = bux_bytes_to_hex(buf, 64); + Free(buf); + return result; +} + +// --- Raw binary output (caller must Alloc/Free) --- + +func Hash_Sha256Raw(data: String, out: *void) { + bux_sha256(data, String_Len(data) as int, out); +} + +func Hash_Sha384Raw(data: String, out: *void) { + bux_sha384(data, String_Len(data) as int, out); +} + +func Hash_Sha512Raw(data: String, out: *void) { + bux_sha512(data, String_Len(data) as int, out); +} + +// --- Digest sizes --- + +func Hash_Sha1Size() -> int { return 20; } +func Hash_Sha256Size() -> int { return 32; } +func Hash_Sha384Size() -> int { return 48; } +func Hash_Sha512Size() -> int { return 64; } +} diff --git a/lib/crypto/hmac.bux b/lib/crypto/hmac.bux new file mode 100644 index 0000000..5768c8e --- /dev/null +++ b/lib/crypto/hmac.bux @@ -0,0 +1,92 @@ +// ============================================================================= +// Std::Crypto::Hmac — HMAC-SHA256, HMAC-SHA384, HMAC-SHA512 +// ============================================================================= +module Std::Crypto::Hmac { + +import Std::Mem::{Alloc, Free}; +import Std::String::{String_Len}; + +extern func bux_hmac_sha256(key: String, keylen: int, msg: String, msglen: int, out: *void); +extern func bux_hmac_sha384(key: String, keylen: int, msg: String, msglen: int, out: *void); +extern func bux_hmac_sha512(key: String, keylen: int, msg: String, msglen: int, out: *void); +extern func bux_bytes_to_hex(data: *void, len: int) -> String; +extern func bux_base64_encode(data: String, len: int) -> String; + +// --- HMAC-SHA256 --- + +func Hmac_Sha256(key: String, message: String) -> String { + let kl: int = String_Len(key) as int; + let ml: int = String_Len(message) as int; + let buf: *void = Alloc(32); + bux_hmac_sha256(key, kl, message, ml, buf); + let result: String = bux_bytes_to_hex(buf, 32); + Free(buf); + return result; +} + +func Hmac_Sha256Raw(key: String, message: String, out: *void) { + bux_hmac_sha256(key, String_Len(key) as int, message, String_Len(message) as int, out); +} + +func Hmac_Sha256Base64(key: String, message: String) -> String { + let kl: int = String_Len(key) as int; + let ml: int = String_Len(message) as int; + let buf: *void = Alloc(32); + bux_hmac_sha256(key, kl, message, ml, buf); + let result: String = bux_base64_encode(buf as String, 32); + Free(buf); + return result; +} + +// --- HMAC-SHA384 --- + +func Hmac_Sha384(key: String, message: String) -> String { + let kl: int = String_Len(key) as int; + let ml: int = String_Len(message) as int; + let buf: *void = Alloc(48); + bux_hmac_sha384(key, kl, message, ml, buf); + let result: String = bux_bytes_to_hex(buf, 48); + Free(buf); + return result; +} + +func Hmac_Sha384Raw(key: String, message: String, out: *void) { + bux_hmac_sha384(key, String_Len(key) as int, message, String_Len(message) as int, out); +} + +func Hmac_Sha384Base64(key: String, message: String) -> String { + let kl: int = String_Len(key) as int; + let ml: int = String_Len(message) as int; + let buf: *void = Alloc(48); + bux_hmac_sha384(key, kl, message, ml, buf); + let result: String = bux_base64_encode(buf as String, 48); + Free(buf); + return result; +} + +// --- HMAC-SHA512 --- + +func Hmac_Sha512(key: String, message: String) -> String { + let kl: int = String_Len(key) as int; + let ml: int = String_Len(message) as int; + let buf: *void = Alloc(64); + bux_hmac_sha512(key, kl, message, ml, buf); + let result: String = bux_bytes_to_hex(buf, 64); + Free(buf); + return result; +} + +func Hmac_Sha512Raw(key: String, message: String, out: *void) { + bux_hmac_sha512(key, String_Len(key) as int, message, String_Len(message) as int, out); +} + +func Hmac_Sha512Base64(key: String, message: String) -> String { + let kl: int = String_Len(key) as int; + let ml: int = String_Len(message) as int; + let buf: *void = Alloc(64); + bux_hmac_sha512(key, kl, message, ml, buf); + let result: String = bux_base64_encode(buf as String, 64); + Free(buf); + return result; +} +} diff --git a/lib/crypto/jwt.bux b/lib/crypto/jwt.bux new file mode 100644 index 0000000..7cf9584 --- /dev/null +++ b/lib/crypto/jwt.bux @@ -0,0 +1,242 @@ +// ============================================================================= +// Std::Crypto::Jwt — JSON Web Token (RFC 7519) encode/decode +// Supports HS256, HS384, HS512, RS256, RS384, RS512, ES256, ES384, EdDSA +// ============================================================================= +module Std::Crypto::Jwt { + +import Std::Mem::{Alloc, Free}; +import Std::String::{String_Len, String_Eq, String_StartsWith, String_Concat}; +import Std::Crypto::Base64::{Base64URL_Encode, Base64URL_Decode}; +import Std::Crypto::Hash::{Hash_Sha256Raw, Hash_Sha384Raw, Hash_Sha512Raw}; +import Std::Crypto::Hmac::{Hmac_Sha256Raw, Hmac_Sha384Raw, Hmac_Sha512Raw}; +import Std::Crypto::Rsa::{Rsa_SignSha256, Rsa_SignSha384, Rsa_SignSha512, + Rsa_VerifySha256, Rsa_VerifySha384, Rsa_VerifySha512}; +import Std::Crypto::Ecdsa::{Ecdsa_SignP256, Ecdsa_SignP384, Ecdsa_VerifyP256, Ecdsa_VerifyP384}; +import Std::Crypto::Ed25519::{Ed25519_Sign, Ed25519_Verify}; + +extern func bux_base64_encode(data: String, len: int) -> String; +extern func bux_str_split_count(s: String, delim: String) -> uint; +extern func bux_str_split_part(s: String, delim: String, index: uint) -> String; + +// --- JWT Algorithm enum --- +enum JwtAlg { + HS256, + HS384, + HS512, + RS256, + RS384, + RS512, + ES256, + ES384, + EdDSA, +} + +// --- Header --- + +// Jwt_MakeHeader: build the JWT header JSON string for the given algorithm +func Jwt_MakeHeader(alg: JwtAlg) -> String { + if alg.tag == JwtAlg_HS256 { return "{\"alg\":\"HS256\",\"typ\":\"JWT\"}"; } + if alg.tag == JwtAlg_HS384 { return "{\"alg\":\"HS384\",\"typ\":\"JWT\"}"; } + if alg.tag == JwtAlg_HS512 { return "{\"alg\":\"HS512\",\"typ\":\"JWT\"}"; } + if alg.tag == JwtAlg_RS256 { return "{\"alg\":\"RS256\",\"typ\":\"JWT\"}"; } + if alg.tag == JwtAlg_RS384 { return "{\"alg\":\"RS384\",\"typ\":\"JWT\"}"; } + if alg.tag == JwtAlg_RS512 { return "{\"alg\":\"RS512\",\"typ\":\"JWT\"}"; } + if alg.tag == JwtAlg_ES256 { return "{\"alg\":\"ES256\",\"typ\":\"JWT\"}"; } + if alg.tag == JwtAlg_ES384 { return "{\"alg\":\"ES384\",\"typ\":\"JWT\"}"; } + if alg.tag == JwtAlg_EdDSA { return "{\"alg\":\"EdDSA\",\"typ\":\"JWT\"}"; } + return "{\"alg\":\"none\",\"typ\":\"JWT\"}"; +} + +// --- Signing --- + +// Sign the JWT signing input with the given algorithm +func Jwt_Sign(alg: JwtAlg, signingInput: String, key: String) -> String { + let algTag: int = alg.tag; + let inputLen: int = String_Len(signingInput) as int; + + // --- HMAC algorithms --- + if algTag == JwtAlg_HS256 { + let buf: *void = Alloc(32); + Hmac_Sha256Raw(key, signingInput, buf); + let result: String = bux_base64_encode(buf as String, 32); + Free(buf); + return result; + } + if algTag == JwtAlg_HS384 { + let buf: *void = Alloc(48); + Hmac_Sha384Raw(key, signingInput, buf); + let result: String = bux_base64_encode(buf as String, 48); + Free(buf); + return result; + } + if algTag == JwtAlg_HS512 { + let buf: *void = Alloc(64); + Hmac_Sha512Raw(key, signingInput, buf); + let result: String = bux_base64_encode(buf as String, 64); + Free(buf); + return result; + } + + // --- RSA algorithms (key is PEM private key) --- + if algTag == JwtAlg_RS256 { + let raw: String = Rsa_SignSha256(key, signingInput); + return bux_base64_encode(raw, String_Len(raw) as int); + } + if algTag == JwtAlg_RS384 { + let raw: String = Rsa_SignSha384(key, signingInput); + return bux_base64_encode(raw, String_Len(raw) as int); + } + if algTag == JwtAlg_RS512 { + let raw: String = Rsa_SignSha512(key, signingInput); + return bux_base64_encode(raw, String_Len(raw) as int); + } + + // --- ECDSA algorithms (key is PEM private key) --- + if algTag == JwtAlg_ES256 { + let raw: String = Ecdsa_SignP256(key, signingInput); + return bux_base64_encode(raw, String_Len(raw) as int); + } + if algTag == JwtAlg_ES384 { + let raw: String = Ecdsa_SignP384(key, signingInput); + return bux_base64_encode(raw, String_Len(raw) as int); + } + + // --- EdDSA (key is 32-byte raw private key) --- + if algTag == JwtAlg_EdDSA { + return Ed25519_Sign(key, signingInput); + } + + return ""; +} + +// --- Verify --- + +// Verify a JWT signature +func Jwt_Verify(alg: JwtAlg, signingInput: String, signatureB64: String, key: String) -> bool { + let algTag: int = alg.tag; + let inputLen: int = String_Len(signingInput) as int; + + // --- HMAC algorithms --- + if algTag == JwtAlg_HS256 { + let expectBuf: *void = Alloc(32); + Hmac_Sha256Raw(key, signingInput, expectBuf); + let expectB64: String = bux_base64_encode(expectBuf as String, 32); + Free(expectBuf); + return String_Eq(expectB64, signatureB64); + } + if algTag == JwtAlg_HS384 { + let expectBuf: *void = Alloc(48); + Hmac_Sha384Raw(key, signingInput, expectBuf); + let expectB64: String = bux_base64_encode(expectBuf as String, 48); + Free(expectBuf); + return String_Eq(expectB64, signatureB64); + } + if algTag == JwtAlg_HS512 { + let expectBuf: *void = Alloc(64); + Hmac_Sha512Raw(key, signingInput, expectBuf); + let expectB64: String = bux_base64_encode(expectBuf as String, 64); + Free(expectBuf); + return String_Eq(expectB64, signatureB64); + } + + // --- RSA algorithms --- + if algTag == JwtAlg_RS256 { return Rsa_VerifySha256(key, signingInput, signatureB64); } + if algTag == JwtAlg_RS384 { return Rsa_VerifySha384(key, signingInput, signatureB64); } + if algTag == JwtAlg_RS512 { return Rsa_VerifySha512(key, signingInput, signatureB64); } + + // --- ECDSA algorithms --- + if algTag == JwtAlg_ES256 { return Ecdsa_VerifyP256(key, signingInput, signatureB64); } + if algTag == JwtAlg_ES384 { return Ecdsa_VerifyP384(key, signingInput, signatureB64); } + + // --- EdDSA --- + if algTag == JwtAlg_EdDSA { return Ed25519_Verify(key, signatureB64, signingInput); } + + return false; +} + +// --- Encode --- + +// Jwt_Encode: create a signed JWT +// headerJson — JSON header string (use Jwt_MakeHeader or custom) +// payloadJson — JSON payload/claims string +// alg — signing algorithm +// key — signing key (HMAC secret, RSA PEM, ECDSA PEM, or Ed25519 raw privkey) +// Returns the complete "header.payload.signature" JWT string +func Jwt_Encode(headerJson: String, payloadJson: String, alg: JwtAlg, key: String) -> String { + let headerB64: String = Base64URL_Encode(headerJson); + let payloadB64: String = Base64URL_Encode(payloadJson); + let signingInput: String = String_Concat(headerB64, "."); + let signingInputFull: String = String_Concat(signingInput, payloadB64); + + let sigB64: String = Jwt_Sign(alg, signingInputFull, key); + + let part1: String = String_Concat(signingInputFull, "."); + return String_Concat(part1, sigB64); +} + +// --- Decode --- + +// Jwt_Decode: decode and verify a JWT. +// token — the full "header.payload.signature" string +// alg — expected algorithm +// key — verification key +// headerOut — receives decoded header JSON +// payloadOut — receives decoded payload JSON +// Returns true if signature is valid. +func Jwt_Decode(token: String, alg: JwtAlg, key: String, + headerOut: *String, payloadOut: *String) -> bool { + // Split by "." + let partCount: uint = bux_str_split_count(token, "."); + if partCount != 3 { return false; } + + let headerB64: String = bux_str_split_part(token, ".", 0); + let payloadB64: String = bux_str_split_part(token, ".", 1); + let sigB64: String = bux_str_split_part(token, ".", 2); + + // Build signing input + let input: String = String_Concat(headerB64, "."); + let signingInput: String = String_Concat(input, payloadB64); + + // Verify signature + if !Jwt_Verify(alg, signingInput, sigB64, key) { + return false; + } + + // Decode + headerOut[0] = Base64URL_Decode(headerB64); + payloadOut[0] = Base64URL_Decode(payloadB64); + return true; +} + +// --- Convenience: Encode with standard header --- + +func Jwt_EncodeHS256(payloadJson: String, secret: String) -> String { + let header: String = Jwt_MakeHeader(JwtAlg { tag: JwtAlg_HS256 }); + return Jwt_Encode(header, payloadJson, JwtAlg { tag: JwtAlg_HS256 }, secret); +} + +func Jwt_EncodeHS384(payloadJson: String, secret: String) -> String { + let header: String = Jwt_MakeHeader(JwtAlg { tag: JwtAlg_HS384 }); + return Jwt_Encode(header, payloadJson, JwtAlg { tag: JwtAlg_HS384 }, secret); +} + +func Jwt_EncodeHS512(payloadJson: String, secret: String) -> String { + let header: String = Jwt_MakeHeader(JwtAlg { tag: JwtAlg_HS512 }); + return Jwt_Encode(header, payloadJson, JwtAlg { tag: JwtAlg_HS512 }, secret); +} + +func Jwt_EncodeRS256(payloadJson: String, pemPrivateKey: String) -> String { + let header: String = Jwt_MakeHeader(JwtAlg { tag: JwtAlg_RS256 }); + return Jwt_Encode(header, payloadJson, JwtAlg { tag: JwtAlg_RS256 }, pemPrivateKey); +} + +func Jwt_EncodeES256(payloadJson: String, pemPrivateKey: String) -> String { + let header: String = Jwt_MakeHeader(JwtAlg { tag: JwtAlg_ES256 }); + return Jwt_Encode(header, payloadJson, JwtAlg { tag: JwtAlg_ES256 }, pemPrivateKey); +} + +func Jwt_EncodeEdDSA(payloadJson: String, rawPrivKey: String) -> String { + let header: String = Jwt_MakeHeader(JwtAlg { tag: JwtAlg_EdDSA }); + return Jwt_Encode(header, payloadJson, JwtAlg { tag: JwtAlg_EdDSA }, rawPrivKey); +} +} diff --git a/lib/crypto/random.bux b/lib/crypto/random.bux new file mode 100644 index 0000000..05fa5df --- /dev/null +++ b/lib/crypto/random.bux @@ -0,0 +1,63 @@ +// ============================================================================= +// Std::Crypto::Random — cryptographically secure random bytes +// ============================================================================= +module Std::Crypto::Random { + +import Std::Mem::{Alloc, Free}; + +extern func bux_random_bytes(buf: *void, len: int) -> int; +extern func bux_base64_encode(data: String, len: int) -> String; +extern func bux_bytes_to_hex(data: *void, len: int) -> String; + +// RandomBytes: returns n cryptographically secure random bytes as a raw string +func Random_Bytes(n: int) -> String { + if n <= 0 { return ""; } + let buf: *void = Alloc(n as uint); + if bux_random_bytes(buf, n) != 1 { + Free(buf); + return ""; + } + // Return raw buffer as string (binary-safe) + return buf as String; +} + +// RandomHex: returns n random bytes as lowercase hex +func Random_Hex(n: int) -> String { + if n <= 0 { return ""; } + let buf: *void = Alloc(n as uint); + if bux_random_bytes(buf, n) != 1 { + Free(buf); + return ""; + } + let result: String = bux_bytes_to_hex(buf, n); + Free(buf); + return result; +} + +// RandomBase64: returns n random bytes as base64-encoded string +func Random_Base64(n: int) -> String { + if n <= 0 { return ""; } + let buf: *void = Alloc(n as uint); + if bux_random_bytes(buf, n) != 1 { + Free(buf); + return ""; + } + let result: String = bux_base64_encode(buf as String, n); + Free(buf); + return result; +} + +// RandomUint32: returns a random 32-bit unsigned integer +func Random_Uint32() -> uint { + let buf: *void = Alloc(4); + if bux_random_bytes(buf, 4) != 1 { + Free(buf); + return 0; + } + // Interpret first 4 bytes as uint (native endian) + let ptr: *uint = buf as *uint; + let val: uint = *ptr; + Free(buf); + return val; +} +} diff --git a/lib/crypto/rsa.bux b/lib/crypto/rsa.bux new file mode 100644 index 0000000..24af15d --- /dev/null +++ b/lib/crypto/rsa.bux @@ -0,0 +1,98 @@ +// ============================================================================= +// Std::Crypto::Rsa — RSA PKCS#1 v1.5 sign/verify (SHA-256, SHA-384, SHA-512) +// ============================================================================= +module Std::Crypto::Rsa { + +import Std::Mem::{Alloc, Free}; +import Std::String::{String_Len}; + +extern func bux_rsa_sign_sha256(pemKey: String, keylen: int, data: String, datalen: int, siglen: *int) -> String; +extern func bux_rsa_sign_sha384(pemKey: String, keylen: int, data: String, datalen: int, siglen: *int) -> String; +extern func bux_rsa_sign_sha512(pemKey: String, keylen: int, data: String, datalen: int, siglen: *int) -> String; +extern func bux_rsa_verify_sha256(pemKey: String, keylen: int, data: String, datalen: int, sig: String, siglen: int) -> int; +extern func bux_rsa_verify_sha384(pemKey: String, keylen: int, data: String, datalen: int, sig: String, siglen: int) -> int; +extern func bux_rsa_verify_sha512(pemKey: String, keylen: int, data: String, datalen: int, sig: String, siglen: int) -> int; +extern func bux_base64_encode(data: String, len: int) -> String; +extern func bux_base64_decode(data: String, len: int, outlen: *int) -> String; + +// --- RSA Sign --- + +// Rsa_SignSha256: sign data with RSA private key (PEM format), returns raw signature +func Rsa_SignSha256(pemPrivateKey: String, data: String) -> String { + let siglen: int = 0; + return bux_rsa_sign_sha256(pemPrivateKey, String_Len(pemPrivateKey) as int, + data, String_Len(data) as int, &siglen); +} + +func Rsa_SignSha384(pemPrivateKey: String, data: String) -> String { + let siglen: int = 0; + return bux_rsa_sign_sha384(pemPrivateKey, String_Len(pemPrivateKey) as int, + data, String_Len(data) as int, &siglen); +} + +func Rsa_SignSha512(pemPrivateKey: String, data: String) -> String { + let siglen: int = 0; + return bux_rsa_sign_sha512(pemPrivateKey, String_Len(pemPrivateKey) as int, + data, String_Len(data) as int, &siglen); +} + +// Convenience: sign and return base64-encoded signature +func Rsa_SignSha256Base64(pemPrivateKey: String, data: String) -> String { + let sig: String = Rsa_SignSha256(pemPrivateKey, data); + return bux_base64_encode(sig, String_Len(sig) as int); +} + +func Rsa_SignSha384Base64(pemPrivateKey: String, data: String) -> String { + let sig: String = Rsa_SignSha384(pemPrivateKey, data); + return bux_base64_encode(sig, String_Len(sig) as int); +} + +func Rsa_SignSha512Base64(pemPrivateKey: String, data: String) -> String { + let sig: String = Rsa_SignSha512(pemPrivateKey, data); + return bux_base64_encode(sig, String_Len(sig) as int); +} + +// --- RSA Verify --- + +// Rsa_VerifySha256: verify raw signature against data with RSA public key (PEM) +// Returns true if signature is valid. +func Rsa_VerifySha256(pemPublicKey: String, data: String, signature: String) -> bool { + let r: int = bux_rsa_verify_sha256(pemPublicKey, String_Len(pemPublicKey) as int, + data, String_Len(data) as int, + signature, String_Len(signature) as int); + return r == 1; +} + +func Rsa_VerifySha384(pemPublicKey: String, data: String, signature: String) -> bool { + let r: int = bux_rsa_verify_sha384(pemPublicKey, String_Len(pemPublicKey) as int, + data, String_Len(data) as int, + signature, String_Len(signature) as int); + return r == 1; +} + +func Rsa_VerifySha512(pemPublicKey: String, data: String, signature: String) -> bool { + let r: int = bux_rsa_verify_sha512(pemPublicKey, String_Len(pemPublicKey) as int, + data, String_Len(data) as int, + signature, String_Len(signature) as int); + return r == 1; +} + +// Convenience: verify base64-encoded signature +func Rsa_VerifySha256Base64(pemPublicKey: String, data: String, signatureB64: String) -> bool { + let outlen: int = 0; + let sig: String = bux_base64_decode(signatureB64, String_Len(signatureB64) as int, &outlen); + return Rsa_VerifySha256(pemPublicKey, data, sig); +} + +func Rsa_VerifySha384Base64(pemPublicKey: String, data: String, signatureB64: String) -> bool { + let outlen: int = 0; + let sig: String = bux_base64_decode(signatureB64, String_Len(signatureB64) as int, &outlen); + return Rsa_VerifySha384(pemPublicKey, data, sig); +} + +func Rsa_VerifySha512Base64(pemPublicKey: String, data: String, signatureB64: String) -> bool { + let outlen: int = 0; + let sig: String = bux_base64_decode(signatureB64, String_Len(signatureB64) as int, &outlen); + return Rsa_VerifySha512(pemPublicKey, data, sig); +} +} diff --git a/rt/runtime.c b/rt/runtime.c index 30c4c5c..7b897c4 100644 --- a/rt/runtime.c +++ b/rt/runtime.c @@ -1322,6 +1322,12 @@ void bux_assert(int cond, const char* file, int line, const char* expr) { #include #include #include +#include +#include +#include +#include +#include +#include void bux_sha256(const char* data, int len, unsigned char* out) { EVP_MD_CTX* ctx = EVP_MD_CTX_new(); @@ -1369,3 +1375,343 @@ char* bux_bytes_to_hex(const unsigned char* data, int len) { out[len * 2] = '\0'; return out; } + +/* ============================================================================ + * Extended cryptography primitives (OpenSSL) + * ============================================================================ */ + +static void bux_digest(const EVP_MD* md, const char* data, int len, unsigned char* out) { + EVP_MD_CTX* ctx = EVP_MD_CTX_new(); + EVP_DigestInit_ex(ctx, md, NULL); + EVP_DigestUpdate(ctx, data, (size_t)len); + EVP_DigestFinal_ex(ctx, out, NULL); + EVP_MD_CTX_free(ctx); +} + +void bux_sha1(const char* data, int len, unsigned char* out) { + bux_digest(EVP_sha1(), data, len, out); +} + +void bux_sha384(const char* data, int len, unsigned char* out) { + bux_digest(EVP_sha384(), data, len, out); +} + +void bux_sha512(const char* data, int len, unsigned char* out) { + bux_digest(EVP_sha512(), data, len, out); +} + +/* --- HMAC --- */ + +void bux_hmac_sha384(const char* key, int keylen, const char* msg, int msglen, unsigned char* out) { + unsigned int outlen = 48; + HMAC(EVP_sha384(), key, keylen, (const unsigned char*)msg, (size_t)msglen, out, &outlen); +} + +void bux_hmac_sha512(const char* key, int keylen, const char* msg, int msglen, unsigned char* out) { + unsigned int outlen = 64; + HMAC(EVP_sha512(), key, keylen, (const unsigned char*)msg, (size_t)msglen, out, &outlen); +} + +/* --- Base64URL --- */ + +static const char b64url_table[] = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"; + +char* bux_base64url_encode(const unsigned char* in, int inlen) { + int outlen = 4 * ((inlen + 2) / 3); + char* out = (char*)bux_alloc(outlen + 1); + int j = 0; + for (int i = 0; i < inlen; i += 3) { + int a = in[i]; + int b = (i + 1 < inlen) ? in[i + 1] : 0; + int c = (i + 2 < inlen) ? in[i + 2] : 0; + out[j++] = b64url_table[(a >> 2) & 0x3F]; + out[j++] = b64url_table[((a << 4) | (b >> 4)) & 0x3F]; + if (i + 1 < inlen) + out[j++] = b64url_table[((b << 2) | (c >> 6)) & 0x3F]; + if (i + 2 < inlen) + out[j++] = b64url_table[c & 0x3F]; + } + out[j] = '\0'; + return out; +} + +static int b64url_char_val(char c) { + if (c >= 'A' && c <= 'Z') return c - 'A'; + if (c >= 'a' && c <= 'z') return c - 'a' + 26; + if (c >= '0' && c <= '9') return c - '0' + 52; + if (c == '-') return 62; + if (c == '_') return 63; + return -1; +} + +char* bux_base64url_decode(const char* in, int inlen, int* outlen) { + int maxlen = 3 * inlen / 4 + 1; + char* out = (char*)bux_alloc(maxlen); + int j = 0; + int buf = 0, bits = 0; + for (int i = 0; i < inlen; i++) { + int val = b64url_char_val(in[i]); + if (val < 0) continue; + buf = (buf << 6) | val; + bits += 6; + if (bits >= 8) { + bits -= 8; + out[j++] = (buf >> bits) & 0xFF; + } + } + out[j] = '\0'; + *outlen = j; + return out; +} + +/* --- AES-256-CBC --- */ + +static int aes_cbc_ctx_encrypt(const unsigned char* in, int inlen, + unsigned char* out, const unsigned char* key, + const unsigned char* iv, int encrypt) { + EVP_CIPHER_CTX* ctx = EVP_CIPHER_CTX_new(); + EVP_CipherInit_ex(ctx, EVP_aes_256_cbc(), NULL, key, iv, encrypt); + int outlen = 0, tmplen = 0; + EVP_CipherUpdate(ctx, out, &tmplen, in, inlen); + outlen = tmplen; + EVP_CipherFinal_ex(ctx, out + outlen, &tmplen); + outlen += tmplen; + EVP_CIPHER_CTX_free(ctx); + return outlen; +} + +char* bux_aes_256_cbc_encrypt(const char* plain, int plainlen, + const char* key, const char* iv, int* outlen) { + int maxlen = plainlen + EVP_MAX_BLOCK_LENGTH; + char* out = (char*)bux_alloc(maxlen + 1); + *outlen = aes_cbc_ctx_encrypt((const unsigned char*)plain, plainlen, + (unsigned char*)out, + (const unsigned char*)key, + (const unsigned char*)iv, 1); + out[*outlen] = '\0'; + return out; +} + +char* bux_aes_256_cbc_decrypt(const char* cipher, int cipherlen, + const char* key, const char* iv, int* outlen) { + char* out = (char*)bux_alloc(cipherlen + 1); + *outlen = aes_cbc_ctx_encrypt((const unsigned char*)cipher, cipherlen, + (unsigned char*)out, + (const unsigned char*)key, + (const unsigned char*)iv, 0); + out[*outlen] = '\0'; + return out; +} + +/* --- AES-256-GCM --- */ + +char* bux_aes_256_gcm_encrypt(const char* plain, int plainlen, + const char* key, const char* iv, + unsigned char* tag, int* outlen) { + EVP_CIPHER_CTX* ctx = EVP_CIPHER_CTX_new(); + EVP_EncryptInit_ex(ctx, EVP_aes_256_gcm(), NULL, (const unsigned char*)key, + (const unsigned char*)iv); + int maxlen = plainlen + EVP_MAX_BLOCK_LENGTH; + char* out = (char*)bux_alloc(maxlen); + int tmplen = 0; + EVP_EncryptUpdate(ctx, (unsigned char*)out, &tmplen, + (const unsigned char*)plain, plainlen); + *outlen = tmplen; + EVP_EncryptFinal_ex(ctx, (unsigned char*)out + *outlen, &tmplen); + *outlen += tmplen; + EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_GET_TAG, 16, tag); + EVP_CIPHER_CTX_free(ctx); + return out; +} + +char* bux_aes_256_gcm_decrypt(const char* cipher, int cipherlen, + const char* key, const char* iv, + const char* tag, int* outlen) { + EVP_CIPHER_CTX* ctx = EVP_CIPHER_CTX_new(); + EVP_DecryptInit_ex(ctx, EVP_aes_256_gcm(), NULL, (const unsigned char*)key, + (const unsigned char*)iv); + char* out = (char*)bux_alloc(cipherlen); + int tmplen = 0; + EVP_DecryptUpdate(ctx, (unsigned char*)out, &tmplen, + (const unsigned char*)cipher, cipherlen); + *outlen = tmplen; + EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_TAG, 16, (void*)tag); + int ret = EVP_DecryptFinal_ex(ctx, (unsigned char*)out + *outlen, &tmplen); + EVP_CIPHER_CTX_free(ctx); + if (ret <= 0) { *outlen = 0; out[0] = '\0'; return out; } + *outlen += tmplen; + out[*outlen] = '\0'; + return out; +} + +/* --- RSA PKCS#1 v1.5 sign / verify --- */ + +static EVP_PKEY* bux_load_private_key(const char* pem, int len) { + BIO* bio = BIO_new_mem_buf(pem, len); + if (!bio) return NULL; + EVP_PKEY* pkey = PEM_read_bio_PrivateKey(bio, NULL, NULL, NULL); + BIO_free(bio); + return pkey; +} + +static EVP_PKEY* bux_load_public_key(const char* pem, int len) { + BIO* bio = BIO_new_mem_buf(pem, len); + if (!bio) return NULL; + EVP_PKEY* pkey = PEM_read_bio_PUBKEY(bio, NULL, NULL, NULL); + BIO_free(bio); + return pkey; +} + +static char* bux_rsa_sign_evp(const EVP_MD* md, const char* pem_key, int keylen, + const char* data, int datalen, int* siglen) { + EVP_PKEY* pkey = bux_load_private_key(pem_key, keylen); + if (!pkey) { *siglen = 0; return (char*)bux_alloc(1); } + EVP_MD_CTX* ctx = EVP_MD_CTX_new(); + EVP_SignInit_ex(ctx, md, NULL); + EVP_SignUpdate(ctx, data, (size_t)datalen); + size_t slen = (size_t)EVP_PKEY_size(pkey); + unsigned char* sig = (unsigned char*)bux_alloc(slen + 1); + EVP_SignFinal(ctx, sig, &slen, pkey); + *siglen = (int)slen; + sig[*siglen] = '\0'; + EVP_MD_CTX_free(ctx); + EVP_PKEY_free(pkey); + return (char*)sig; +} + +static int bux_rsa_verify_evp(const EVP_MD* md, const char* pem_key, int keylen, + const char* data, int datalen, + const char* sig, int siglen) { + EVP_PKEY* pkey = bux_load_public_key(pem_key, keylen); + if (!pkey) return 0; + EVP_MD_CTX* ctx = EVP_MD_CTX_new(); + EVP_VerifyInit_ex(ctx, md, NULL); + EVP_VerifyUpdate(ctx, data, (size_t)datalen); + int ret = EVP_VerifyFinal(ctx, (const unsigned char*)sig, (size_t)siglen, pkey); + EVP_MD_CTX_free(ctx); + EVP_PKEY_free(pkey); + return ret; +} + +char* bux_rsa_sign_sha256(const char* pem, int keylen, const char* data, int datalen, int* siglen) { + return bux_rsa_sign_evp(EVP_sha256(), pem, keylen, data, datalen, siglen); +} +char* bux_rsa_sign_sha384(const char* pem, int keylen, const char* data, int datalen, int* siglen) { + return bux_rsa_sign_evp(EVP_sha384(), pem, keylen, data, datalen, siglen); +} +char* bux_rsa_sign_sha512(const char* pem, int keylen, const char* data, int datalen, int* siglen) { + return bux_rsa_sign_evp(EVP_sha512(), pem, keylen, data, datalen, siglen); +} + +int bux_rsa_verify_sha256(const char* pem, int keylen, const char* data, int datalen, + const char* sig, int siglen) { + return bux_rsa_verify_evp(EVP_sha256(), pem, keylen, data, datalen, sig, siglen); +} +int bux_rsa_verify_sha384(const char* pem, int keylen, const char* data, int datalen, + const char* sig, int siglen) { + return bux_rsa_verify_evp(EVP_sha384(), pem, keylen, data, datalen, sig, siglen); +} +int bux_rsa_verify_sha512(const char* pem, int keylen, const char* data, int datalen, + const char* sig, int siglen) { + return bux_rsa_verify_evp(EVP_sha512(), pem, keylen, data, datalen, sig, siglen); +} + +/* --- ECDSA P-256 / P-384 --- */ + +static char* bux_ecdsa_sign_evp(const EVP_MD* md, const char* pem, int keylen, + const char* data, int datalen, int* siglen) { + EVP_PKEY* pkey = bux_load_private_key(pem, keylen); + if (!pkey) { *siglen = 0; return (char*)bux_alloc(1); } + EVP_MD_CTX* ctx = EVP_MD_CTX_new(); + EVP_SignInit_ex(ctx, md, NULL); + EVP_SignUpdate(ctx, data, (size_t)datalen); + size_t slen = (size_t)EVP_PKEY_size(pkey); + unsigned char* sig = (unsigned char*)bux_alloc(slen + 1); + EVP_SignFinal(ctx, sig, &slen, pkey); + *siglen = (int)slen; + sig[*siglen] = '\0'; + EVP_MD_CTX_free(ctx); + EVP_PKEY_free(pkey); + return (char*)sig; +} + +static int bux_ecdsa_verify_evp(const EVP_MD* md, const char* pem, int keylen, + const char* data, int datalen, + const char* sig, int siglen) { + EVP_PKEY* pkey = bux_load_public_key(pem, keylen); + if (!pkey) return 0; + EVP_MD_CTX* ctx = EVP_MD_CTX_new(); + EVP_VerifyInit_ex(ctx, md, NULL); + EVP_VerifyUpdate(ctx, data, (size_t)datalen); + int ret = EVP_VerifyFinal(ctx, (const unsigned char*)sig, (size_t)siglen, pkey); + EVP_MD_CTX_free(ctx); + EVP_PKEY_free(pkey); + return ret; +} + +char* bux_ecdsa_sign_p256(const char* pem, int keylen, const char* data, int datalen, int* siglen) { + return bux_ecdsa_sign_evp(EVP_sha256(), pem, keylen, data, datalen, siglen); +} +char* bux_ecdsa_sign_p384(const char* pem, int keylen, const char* data, int datalen, int* siglen) { + return bux_ecdsa_sign_evp(EVP_sha384(), pem, keylen, data, datalen, siglen); +} +int bux_ecdsa_verify_p256(const char* pem, int keylen, const char* data, int datalen, + const char* sig, int siglen) { + return bux_ecdsa_verify_evp(EVP_sha256(), pem, keylen, data, datalen, sig, siglen); +} +int bux_ecdsa_verify_p384(const char* pem, int keylen, const char* data, int datalen, + const char* sig, int siglen) { + return bux_ecdsa_verify_evp(EVP_sha384(), pem, keylen, data, datalen, sig, siglen); +} + +/* --- Ed25519 (OpenSSL 1.1.1+) --- */ + +int bux_ed25519_keypair(unsigned char* pub, unsigned char* priv) { + EVP_PKEY* pkey = NULL; + EVP_PKEY_CTX* pctx = EVP_PKEY_CTX_new_id(EVP_PKEY_ED25519, NULL); + if (!pctx) return 0; + if (EVP_PKEY_keygen_init(pctx) <= 0 || + EVP_PKEY_keygen(pctx, &pkey) <= 0) { + EVP_PKEY_CTX_free(pctx); + return 0; + } + EVP_PKEY_CTX_free(pctx); + size_t pub_len = 32, priv_len = 32; + EVP_PKEY_get_raw_public_key(pkey, pub, &pub_len); + EVP_PKEY_get_raw_private_key(pkey, priv, &priv_len); + EVP_PKEY_free(pkey); + return 1; +} + +int bux_ed25519_sign(const char* priv, const char* data, int datalen, unsigned char* sig) { + EVP_PKEY* pkey = EVP_PKEY_new_raw_private_key(EVP_PKEY_ED25519, NULL, + (const unsigned char*)priv, 32); + if (!pkey) return 0; + EVP_MD_CTX* ctx = EVP_MD_CTX_new(); + int ret = 0; + if (EVP_DigestSignInit(ctx, NULL, NULL, NULL, pkey) > 0) { + size_t siglen = 64; + EVP_DigestSign(ctx, sig, &siglen, (const unsigned char*)data, (size_t)datalen); + ret = 1; + } + EVP_MD_CTX_free(ctx); + EVP_PKEY_free(pkey); + return ret; +} + +int bux_ed25519_verify(const char* pub, const char* sig, const char* data, int datalen) { + EVP_PKEY* pkey = EVP_PKEY_new_raw_public_key(EVP_PKEY_ED25519, NULL, + (const unsigned char*)pub, 32); + if (!pkey) return 0; + EVP_MD_CTX* ctx = EVP_MD_CTX_new(); + int ret = 0; + if (EVP_DigestVerifyInit(ctx, NULL, NULL, NULL, pkey) > 0) { + ret = EVP_DigestVerify(ctx, (const unsigned char*)sig, 64, + (const unsigned char*)data, (size_t)datalen); + ret = (ret == 1) ? 1 : 0; + } + EVP_MD_CTX_free(ctx); + EVP_PKEY_free(pkey); + return ret; +} diff --git a/test_crypto/build.log b/test_crypto/build.log new file mode 100644 index 0000000..903634c --- /dev/null +++ b/test_crypto/build.log @@ -0,0 +1 @@ +EXIT:137 diff --git a/test_crypto/bux.toml b/test_crypto/bux.toml new file mode 100644 index 0000000..b7180a6 --- /dev/null +++ b/test_crypto/bux.toml @@ -0,0 +1,7 @@ +[Package] +Name = "test_crypto" +Version = "0.1.0" +Type = "bin" + +[Build] +Output = "Bin" diff --git a/test_crypto/src/Main.bux b/test_crypto/src/Main.bux new file mode 100644 index 0000000..6d41ca7 --- /dev/null +++ b/test_crypto/src/Main.bux @@ -0,0 +1,180 @@ +// ============================================================================= +// Crypto Library Test — exercises all modules in lib/crypto/ +// ============================================================================= +module Main { + +import Std::Io::{PrintLine, Print, PrintInt}; +import Std::String::{String_Len, String_Eq}; +import Std::Crypto::Base64::{Base64_Encode, Base64_Decode, Base64URL_Encode, Base64URL_Decode}; +import Std::Crypto::Hash::{Hash_Sha256, Hash_Sha384, Hash_Sha512, Hash_Sha256Size, Hash_Sha384Size, Hash_Sha512Size}; +import Std::Crypto::Hmac::{Hmac_Sha256, Hmac_Sha384, Hmac_Sha512}; +import Std::Crypto::Random::{Random_Bytes, Random_Hex, Random_Base64, Random_Uint32}; +import Std::Crypto::Jwt::{JwtAlg, Jwt_MakeHeader, Jwt_EncodeHS256, Jwt_Decode}; + +// --- Minimal test framework --- +var gPassed: int = 0; +var gFailed: int = 0; + +func Assert(cond: bool, name: String) { + if cond { + gPassed = gPassed + 1; + Print(" PASS "); + } else { + gFailed = gFailed + 1; + Print(" FAIL "); + } + PrintLine(name); +} + +// ============================================================================= +// Test: Base64 + Base64URL +// ============================================================================= +func TestBase64() { + PrintLine("--- Base64 ---"); + + let encoded: String = Base64_Encode("hello"); + Assert(String_Eq(encoded, "aGVsbG8="), "Base64_Encode('hello')"); + + let decoded: String = Base64_Decode("aGVsbG8="); + Assert(String_Eq(decoded, "hello"), "Base64_Decode('aGVsbG8=')"); + + let urlEnc: String = Base64URL_Encode("hello"); + Assert(String_Eq(urlEnc, "aGVsbG8"), "Base64URL_Encode('hello') — no padding"); + + let urlDec: String = Base64URL_Decode("aGVsbG8"); + Assert(String_Eq(urlDec, "hello"), "Base64URL_Decode('aGVsbG8')"); + + let original: String = "Bux crypto test!"; + let rt: String = Base64_Decode(Base64_Encode(original)); + Assert(String_Eq(rt, original), "Base64 round-trip"); + + let rtUrl: String = Base64URL_Decode(Base64URL_Encode(original)); + Assert(String_Eq(rtUrl, original), "Base64URL round-trip"); +} + +// ============================================================================= +// Test: Hash (SHA-256, SHA-384, SHA-512) +// ============================================================================= +func TestHash() { + PrintLine("--- Hash ---"); + + let sha256: String = Hash_Sha256("hello"); + Assert(String_Eq(sha256, + "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824"), + "Hash_Sha256('hello')"); + + let sha384: String = Hash_Sha384("hello"); + Assert(String_Eq(sha384, + "59e1748777448c69de6b800d7a33bbfb9ff1b463e44354c3553bcdb9c666fa90125a3c79f90397bdf5f6a13de828684f"), + "Hash_Sha384('hello')"); + + let sha512: String = Hash_Sha512("hello"); + Assert(String_Eq(sha512, + "9b71d224bd62f3785d96d46ad3ea3d73319bfbc2890caadae2dff72519673ca72323c3d99ba5c11d7c7acc6e14b8c5da0c4663475c2e5c3adef46f73bcdec043"), + "Hash_Sha512('hello')"); + + let empty256: String = Hash_Sha256(""); + Assert(String_Len(empty256) == 64, "Hash_Sha256('') has 64 hex chars"); + + Assert(Hash_Sha256Size() == 32, "Hash_Sha256Size() == 32"); + Assert(Hash_Sha384Size() == 48, "Hash_Sha384Size() == 48"); + Assert(Hash_Sha512Size() == 64, "Hash_Sha512Size() == 64"); +} + +// ============================================================================= +// Test: HMAC +// ============================================================================= +func TestHmac() { + PrintLine("--- HMAC ---"); + + let hmac256: String = Hmac_Sha256("secret", "hello"); + Assert(String_Eq(hmac256, + "88aab3ede8d3adf94d26ab90d3bafd4a2083070c3daec8e50b3899a4a5d1e0f8"), + "Hmac_Sha256('secret', 'hello')"); + + Assert(String_Len(Hmac_Sha384("s", "h")) == 96, "Hmac_Sha384 produces 96 hex chars"); + Assert(String_Len(Hmac_Sha512("s", "h")) == 128, "Hmac_Sha512 produces 128 hex chars"); + + let h1: String = Hmac_Sha256("key", "msg"); + let h2: String = Hmac_Sha256("key", "msg"); + Assert(String_Eq(h1, h2), "HMAC deterministic"); +} + +// ============================================================================= +// Test: Random +// ============================================================================= +func TestRandom() { + PrintLine("--- Random ---"); + + Assert(String_Len(Random_Bytes(32)) == 32, "Random_Bytes(32) len=32"); + Assert(String_Len(Random_Hex(16)) == 32, "Random_Hex(16) len=32"); + Assert(String_Len(Random_Base64(16)) > 0, "Random_Base64(16) non-empty"); +} + +// ============================================================================= +// Test: JWT HS256 encode/decode +// ============================================================================= +func TestJwtHS256() { + PrintLine("--- JWT HS256 ---"); + + let token: String = Jwt_EncodeHS256("{\"sub\":\"test\"}", "secret"); + Assert(String_Len(token) > 20, "Jwt_EncodeHS256 returns token"); + + var header: String = ""; + var payload: String = ""; + let alg: JwtAlg = JwtAlg { tag: JwtAlg_HS256 }; + let ok: bool = Jwt_Decode(token, alg, "secret", &header, &payload); + Assert(ok, "Jwt_Decode HS256 verifies"); + Assert(String_Eq(payload, "{\"sub\":\"test\"}"), "Jwt_Decode returns correct payload"); + + let wrongOk: bool = Jwt_Decode(token, alg, "wrong", &header, &payload); + Assert(!wrongOk, "Jwt_Decode rejects wrong secret"); +} + +// ============================================================================= +// Test: JWT algorithm headers +// ============================================================================= +func TestJwtHeaders() { + PrintLine("--- JWT Headers ---"); + + Assert(String_Eq(Jwt_MakeHeader(JwtAlg { tag: JwtAlg_HS256 }), + "{\"alg\":\"HS256\",\"typ\":\"JWT\"}"), "JWT header HS256"); + Assert(String_Eq(Jwt_MakeHeader(JwtAlg { tag: JwtAlg_RS256 }), + "{\"alg\":\"RS256\",\"typ\":\"JWT\"}"), "JWT header RS256"); + Assert(String_Eq(Jwt_MakeHeader(JwtAlg { tag: JwtAlg_ES256 }), + "{\"alg\":\"ES256\",\"typ\":\"JWT\"}"), "JWT header ES256"); + Assert(String_Eq(Jwt_MakeHeader(JwtAlg { tag: JwtAlg_EdDSA }), + "{\"alg\":\"EdDSA\",\"typ\":\"JWT\"}"), "JWT header EdDSA"); +} + +// ============================================================================= +// Main +// ============================================================================= +func Main() -> int { + PrintLine("================================================"); + PrintLine(" Bux Crypto Library — Test Suite"); + PrintLine("================================================"); + PrintLine(""); + + TestBase64(); + TestHash(); + TestHmac(); + TestRandom(); + TestJwtHS256(); + TestJwtHeaders(); + + PrintLine(""); + PrintLine("================================================"); + Print(" Passed: "); + PrintInt(gPassed); + PrintLine(""); + Print(" Failed: "); + PrintInt(gFailed); + PrintLine(""); + PrintLine("================================================"); + + if gFailed > 0 { return 1; } + return 0; +} + +} // module Main