mirror of
https://github.com/DeBrosOfficial/orama.git
synced 2026-06-17 02:14:12 +00:00
150 lines
6.0 KiB
Zig
150 lines
6.0 KiB
Zig
/// POST /v1/vault/push — Store a share for a user.
|
|
///
|
|
/// Expects JSON body: {"identity":"<hex>","share":"<base64>","index":<n>,"version":<n>}
|
|
/// Stores the share to disk with HMAC integrity via file_store.
|
|
const std = @import("std");
|
|
const response = @import("response.zig");
|
|
const router = @import("router.zig");
|
|
const log = @import("../log.zig");
|
|
const file_store = @import("../storage/file_store.zig");
|
|
const handler_auth = @import("handler_auth.zig");
|
|
|
|
/// Maximum request body size (1 MiB). Prevents memory exhaustion from oversized payloads.
|
|
const MAX_BODY_SIZE = 1024 * 1024;
|
|
/// Maximum decoded share size (512 KiB). Encrypted Shamir shares should be small.
|
|
const MAX_SHARE_SIZE = 512 * 1024;
|
|
/// Identity hash must be exactly 64 hex chars (SHA-256 output).
|
|
const IDENTITY_HEX_LEN = 64;
|
|
|
|
pub fn handle(writer: anytype, body: []const u8, ctx: *const router.RouteContext, session_token: ?[]const u8) !void {
|
|
if (body.len == 0) {
|
|
return response.badRequest(writer, "empty body");
|
|
}
|
|
|
|
// Reject oversized request bodies before parsing
|
|
if (body.len > MAX_BODY_SIZE) {
|
|
return response.badRequest(writer, "request body too large");
|
|
}
|
|
|
|
// Require session token when guardian is configured
|
|
if (ctx.guardian) |guardian| {
|
|
const tok = session_token orelse {
|
|
return response.jsonError(writer, 401, "Unauthorized", "session token required");
|
|
};
|
|
if (handler_auth.validateSessionToken(tok, guardian.server_secret) == null) {
|
|
return response.jsonError(writer, 401, "Unauthorized", "invalid session token");
|
|
}
|
|
}
|
|
|
|
// Parse JSON
|
|
const PushBody = struct {
|
|
identity: []const u8,
|
|
share: []const u8,
|
|
version: u64,
|
|
};
|
|
|
|
const parsed = std.json.parseFromSlice(PushBody, ctx.allocator, body, .{}) catch {
|
|
return response.badRequest(writer, "invalid JSON");
|
|
};
|
|
defer parsed.deinit();
|
|
|
|
const identity = parsed.value.identity;
|
|
const share_b64 = parsed.value.share;
|
|
const version = parsed.value.version;
|
|
|
|
// Validate identity is exactly 64 hex chars (SHA-256 hash)
|
|
if (identity.len != IDENTITY_HEX_LEN) {
|
|
return response.badRequest(writer, "identity must be exactly 64 hex characters");
|
|
}
|
|
for (identity) |c| {
|
|
if (!std.ascii.isHex(c)) {
|
|
return response.badRequest(writer, "identity must be hex");
|
|
}
|
|
}
|
|
|
|
// Decode base64 share
|
|
const decoded_len = std.base64.standard.Decoder.calcSizeForSlice(share_b64) catch {
|
|
return response.badRequest(writer, "invalid base64 in share");
|
|
};
|
|
|
|
// Reject oversized shares before allocating
|
|
if (decoded_len > MAX_SHARE_SIZE) {
|
|
return response.badRequest(writer, "share data too large");
|
|
}
|
|
|
|
const share_data = ctx.allocator.alloc(u8, decoded_len) catch {
|
|
return response.internalError(writer);
|
|
};
|
|
defer ctx.allocator.free(share_data);
|
|
|
|
std.base64.standard.Decoder.decode(share_data, share_b64) catch {
|
|
return response.badRequest(writer, "invalid base64 in share");
|
|
};
|
|
|
|
if (share_data.len == 0) {
|
|
return response.badRequest(writer, "share data is empty");
|
|
}
|
|
|
|
// Anti-rollback: reject shares with version <= current stored version
|
|
const current_version = readCurrentVersion(ctx.data_dir, identity, ctx.allocator);
|
|
if (current_version) |cur_ver| {
|
|
if (version <= cur_ver) {
|
|
log.warn("rejected rollback for {s}: version {d} <= current {d}", .{ identity, version, cur_ver });
|
|
return response.badRequest(writer, "version must be greater than current stored version");
|
|
}
|
|
}
|
|
|
|
// Derive integrity key from guardian server_secret (or use fallback)
|
|
const integrity_key: []const u8 = if (ctx.guardian) |guardian|
|
|
&guardian.server_secret
|
|
else
|
|
"vault-default-integrity-key!!!!!";
|
|
|
|
// Write share data to storage with HMAC integrity
|
|
file_store.writeShare(ctx.data_dir, identity, share_data, integrity_key, ctx.allocator) catch |err| {
|
|
log.err("failed to write share for {s}: {}", .{ identity, err });
|
|
return response.internalError(writer);
|
|
};
|
|
|
|
// Write version file
|
|
writeVersionFile(ctx.data_dir, identity, version) catch |err| {
|
|
log.err("failed to write version for {s}: {}", .{ identity, err });
|
|
// Share was written but version wasn't — not fatal, but log it
|
|
};
|
|
|
|
log.info("stored share for identity {s} ({d} bytes, version {d})", .{ identity, share_data.len, version });
|
|
try response.jsonOk(writer, "{\"status\":\"stored\"}");
|
|
}
|
|
|
|
/// Read the current stored version for an identity. Returns null if no version file exists.
|
|
fn readCurrentVersion(data_dir: []const u8, identity: []const u8, allocator: std.mem.Allocator) ?u64 {
|
|
var path_buf: [4096]u8 = undefined;
|
|
const version_path = std.fmt.bufPrint(&path_buf, "{s}/shares/{s}/version", .{ data_dir, identity }) catch return null;
|
|
|
|
const version_data = std.fs.cwd().readFileAlloc(allocator, version_path, 32) catch return null;
|
|
defer allocator.free(version_data);
|
|
|
|
return std.fmt.parseInt(u64, version_data, 10) catch null;
|
|
}
|
|
|
|
/// Write version counter atomically to: <data_dir>/shares/<identity>/version
|
|
fn writeVersionFile(data_dir: []const u8, identity: []const u8, version: u64) !void {
|
|
var path_buf: [4096]u8 = undefined;
|
|
const tmp_path = std.fmt.bufPrint(&path_buf, "{s}/shares/{s}/version.tmp", .{ data_dir, identity }) catch return error.PathTooLong;
|
|
|
|
var path_buf2: [4096]u8 = undefined;
|
|
const final_path = std.fmt.bufPrint(&path_buf2, "{s}/shares/{s}/version", .{ data_dir, identity }) catch return error.PathTooLong;
|
|
|
|
var ver_buf: [20]u8 = undefined; // max u64 is 20 digits
|
|
const ver_str = std.fmt.bufPrint(&ver_buf, "{d}", .{version}) catch return error.PathTooLong;
|
|
|
|
const file = try std.fs.cwd().createFile(tmp_path, .{});
|
|
defer file.close();
|
|
try file.writeAll(ver_str);
|
|
|
|
std.fs.cwd().rename(tmp_path, final_path) catch |rename_err| {
|
|
std.fs.cwd().deleteFile(tmp_path) catch {};
|
|
return rename_err;
|
|
};
|
|
}
|