mirror of
https://github.com/DeBrosOfficial/orama-vault.git
synced 2026-03-16 19:43:01 +00:00
674 lines
24 KiB
Zig
674 lines
24 KiB
Zig
/// Multi-secret storage engine (V2).
|
|
///
|
|
/// Each identity can store up to MAX_SECRETS_PER_IDENTITY named secrets.
|
|
/// Secrets are stored with HMAC integrity, anti-rollback version checks,
|
|
/// and atomic writes (tmp+rename).
|
|
///
|
|
/// Directory layout:
|
|
/// <data_dir>/vaults/<identity_hex>/
|
|
/// <secret_name>/
|
|
/// share.bin - Encrypted share data
|
|
/// checksum.bin - HMAC-SHA256 integrity
|
|
/// meta.json - {"version":1,"created_ns":...,"updated_ns":...,"size":123}
|
|
const std = @import("std");
|
|
const hmac = @import("../crypto/hmac.zig");
|
|
|
|
// ── Constants ────────────────────────────────────────────────────────────────
|
|
|
|
pub const MAX_SECRETS_PER_IDENTITY = 1000;
|
|
pub const MAX_SECRET_NAME_LEN = 128;
|
|
pub const MAX_SECRET_SIZE = 512 * 1024; // 512 KiB
|
|
|
|
/// Characters allowed in secret names: alphanumeric, underscore, hyphen.
|
|
fn isValidNameChar(c: u8) bool {
|
|
return std.ascii.isAlphanumeric(c) or c == '_' or c == '-';
|
|
}
|
|
|
|
// ── Types ────────────────────────────────────────────────────────────────────
|
|
|
|
pub const SecretMeta = struct {
|
|
version: u64,
|
|
created_ns: i128,
|
|
updated_ns: i128,
|
|
size: usize,
|
|
};
|
|
|
|
pub const VaultStoreError = error{
|
|
IdentityRequired,
|
|
SecretNameRequired,
|
|
SecretNameTooLong,
|
|
SecretNameInvalid,
|
|
SecretDataRequired,
|
|
SecretDataTooLarge,
|
|
SecretLimitExceeded,
|
|
IntegrityCheckFailed,
|
|
VersionConflict,
|
|
NotFound,
|
|
OutOfMemory,
|
|
IoError,
|
|
};
|
|
|
|
// ── Public API ───────────────────────────────────────────────────────────────
|
|
|
|
/// Validates a secret name: non-empty, within length limit, alphanumeric + '_' + '-'.
|
|
pub fn validateSecretName(name: []const u8) VaultStoreError!void {
|
|
if (name.len == 0) return VaultStoreError.SecretNameRequired;
|
|
if (name.len > MAX_SECRET_NAME_LEN) return VaultStoreError.SecretNameTooLong;
|
|
for (name) |c| {
|
|
if (!isValidNameChar(c)) return VaultStoreError.SecretNameInvalid;
|
|
}
|
|
}
|
|
|
|
/// Writes a named secret atomically with HMAC integrity and anti-rollback protection.
|
|
///
|
|
/// For new secrets, checks the per-identity secret count limit.
|
|
/// For existing secrets, rejects if `version` <= the stored version.
|
|
/// Writes share.bin, checksum.bin, and meta.json atomically (tmp+rename).
|
|
pub fn writeSecret(
|
|
data_dir: []const u8,
|
|
identity: []const u8,
|
|
name: []const u8,
|
|
data: []const u8,
|
|
version: u64,
|
|
integrity_key: []const u8,
|
|
allocator: std.mem.Allocator,
|
|
) VaultStoreError!void {
|
|
if (identity.len == 0) return VaultStoreError.IdentityRequired;
|
|
try validateSecretName(name);
|
|
if (data.len == 0) return VaultStoreError.SecretDataRequired;
|
|
if (data.len > MAX_SECRET_SIZE) return VaultStoreError.SecretDataTooLarge;
|
|
|
|
// Build secret directory path: <data_dir>/vaults/<identity>/<name>/
|
|
const secret_dir = std.fmt.allocPrint(allocator, "{s}/vaults/{s}/{s}", .{ data_dir, identity, name }) catch
|
|
return VaultStoreError.OutOfMemory;
|
|
defer allocator.free(secret_dir);
|
|
|
|
// Check if this is a new secret
|
|
const is_new = !secretExistsInternal(data_dir, identity, name, allocator);
|
|
|
|
if (is_new) {
|
|
// Check secret count limit
|
|
const count = countSecrets(data_dir, identity, allocator) catch 0;
|
|
if (count >= MAX_SECRETS_PER_IDENTITY) return VaultStoreError.SecretLimitExceeded;
|
|
} else {
|
|
// Anti-rollback: reject if version <= existing
|
|
const existing_meta = readMeta(data_dir, identity, name, allocator) catch |err| {
|
|
// If we can't read meta but the dir exists, allow overwrite
|
|
if (err == VaultStoreError.IoError) return VaultStoreError.IoError;
|
|
// For NotFound, treat as new
|
|
if (err == VaultStoreError.NotFound) {
|
|
// directory exists but meta doesn't - allow write
|
|
return writeSecretInner(secret_dir, data, version, integrity_key, null, allocator);
|
|
}
|
|
return err;
|
|
};
|
|
if (version <= existing_meta.version) return VaultStoreError.VersionConflict;
|
|
}
|
|
|
|
// Determine created_ns: keep existing for updates, new timestamp for new secrets
|
|
const created_ns: ?i128 = if (!is_new) blk: {
|
|
const existing_meta = readMeta(data_dir, identity, name, allocator) catch break :blk null;
|
|
break :blk existing_meta.created_ns;
|
|
} else null;
|
|
|
|
return writeSecretInner(secret_dir, data, version, integrity_key, created_ns, allocator);
|
|
}
|
|
|
|
/// Reads a named secret and verifies HMAC integrity.
|
|
/// Caller must free the returned slice.
|
|
pub fn readSecret(
|
|
data_dir: []const u8,
|
|
identity: []const u8,
|
|
name: []const u8,
|
|
integrity_key: []const u8,
|
|
allocator: std.mem.Allocator,
|
|
) VaultStoreError![]u8 {
|
|
// Build paths
|
|
const share_path = std.fmt.allocPrint(allocator, "{s}/vaults/{s}/{s}/share.bin", .{ data_dir, identity, name }) catch
|
|
return VaultStoreError.OutOfMemory;
|
|
defer allocator.free(share_path);
|
|
|
|
const checksum_path = std.fmt.allocPrint(allocator, "{s}/vaults/{s}/{s}/checksum.bin", .{ data_dir, identity, name }) catch
|
|
return VaultStoreError.OutOfMemory;
|
|
defer allocator.free(checksum_path);
|
|
|
|
// Read share data
|
|
const share_data = std.fs.cwd().readFileAlloc(allocator, share_path, MAX_SECRET_SIZE) catch {
|
|
return VaultStoreError.NotFound;
|
|
};
|
|
errdefer allocator.free(share_data);
|
|
|
|
// Read checksum
|
|
const checksum_data = std.fs.cwd().readFileAlloc(allocator, checksum_path, 32) catch {
|
|
return VaultStoreError.IoError;
|
|
};
|
|
defer allocator.free(checksum_data);
|
|
|
|
if (checksum_data.len != 32) {
|
|
return VaultStoreError.IntegrityCheckFailed;
|
|
}
|
|
|
|
// Verify HMAC
|
|
if (!hmac.verify(integrity_key, share_data, checksum_data[0..32].*)) {
|
|
return VaultStoreError.IntegrityCheckFailed;
|
|
}
|
|
|
|
return share_data;
|
|
}
|
|
|
|
/// Reads and parses meta.json for a named secret.
|
|
pub fn readMeta(
|
|
data_dir: []const u8,
|
|
identity: []const u8,
|
|
name: []const u8,
|
|
allocator: std.mem.Allocator,
|
|
) VaultStoreError!SecretMeta {
|
|
const meta_path = std.fmt.allocPrint(allocator, "{s}/vaults/{s}/{s}/meta.json", .{ data_dir, identity, name }) catch
|
|
return VaultStoreError.OutOfMemory;
|
|
defer allocator.free(meta_path);
|
|
|
|
const meta_data = std.fs.cwd().readFileAlloc(allocator, meta_path, 4096) catch {
|
|
return VaultStoreError.NotFound;
|
|
};
|
|
defer allocator.free(meta_data);
|
|
|
|
const parsed = std.json.parseFromSlice(MetaJson, allocator, meta_data, .{}) catch {
|
|
return VaultStoreError.IoError;
|
|
};
|
|
defer parsed.deinit();
|
|
|
|
return SecretMeta{
|
|
.version = parsed.value.version,
|
|
.created_ns = parsed.value.created_ns,
|
|
.updated_ns = parsed.value.updated_ns,
|
|
.size = parsed.value.size,
|
|
};
|
|
}
|
|
|
|
/// Deletes a named secret (removes the entire <name>/ directory).
|
|
pub fn deleteSecret(
|
|
data_dir: []const u8,
|
|
identity: []const u8,
|
|
name: []const u8,
|
|
allocator: std.mem.Allocator,
|
|
) VaultStoreError!void {
|
|
const secret_dir = std.fmt.allocPrint(allocator, "{s}/vaults/{s}/{s}", .{ data_dir, identity, name }) catch
|
|
return VaultStoreError.OutOfMemory;
|
|
defer allocator.free(secret_dir);
|
|
|
|
// Check it exists first
|
|
std.fs.cwd().access(secret_dir, .{}) catch {
|
|
return VaultStoreError.NotFound;
|
|
};
|
|
|
|
std.fs.cwd().deleteTree(secret_dir) catch {
|
|
return VaultStoreError.IoError;
|
|
};
|
|
}
|
|
|
|
/// Checks if a named secret exists for the given identity.
|
|
pub fn secretExists(
|
|
data_dir: []const u8,
|
|
identity: []const u8,
|
|
name: []const u8,
|
|
allocator: std.mem.Allocator,
|
|
) !bool {
|
|
return secretExistsInternal(data_dir, identity, name, allocator);
|
|
}
|
|
|
|
/// Lists all secret names for an identity.
|
|
/// Caller must free each name slice and the returned slice.
|
|
pub fn listSecrets(
|
|
data_dir: []const u8,
|
|
identity: []const u8,
|
|
allocator: std.mem.Allocator,
|
|
) ![][]const u8 {
|
|
const vault_dir = std.fmt.allocPrint(allocator, "{s}/vaults/{s}", .{ data_dir, identity }) catch
|
|
return VaultStoreError.OutOfMemory;
|
|
defer allocator.free(vault_dir);
|
|
|
|
var dir = std.fs.cwd().openDir(vault_dir, .{ .iterate = true }) catch {
|
|
// No vault dir = no secrets
|
|
return allocator.alloc([]const u8, 0) catch return VaultStoreError.OutOfMemory;
|
|
};
|
|
defer dir.close();
|
|
|
|
var names: std.ArrayListUnmanaged([]const u8) = .{};
|
|
errdefer {
|
|
for (names.items) |n| allocator.free(n);
|
|
names.deinit(allocator);
|
|
}
|
|
|
|
var it = dir.iterate();
|
|
while (try it.next()) |entry| {
|
|
if (entry.kind == .directory) {
|
|
const name_copy = allocator.dupe(u8, entry.name) catch return VaultStoreError.OutOfMemory;
|
|
names.append(allocator, name_copy) catch {
|
|
allocator.free(name_copy);
|
|
return VaultStoreError.OutOfMemory;
|
|
};
|
|
}
|
|
}
|
|
|
|
return names.toOwnedSlice(allocator) catch return VaultStoreError.OutOfMemory;
|
|
}
|
|
|
|
/// Counts the number of secrets for an identity.
|
|
pub fn countSecrets(
|
|
data_dir: []const u8,
|
|
identity: []const u8,
|
|
allocator: std.mem.Allocator,
|
|
) !usize {
|
|
const vault_dir = std.fmt.allocPrint(allocator, "{s}/vaults/{s}", .{ data_dir, identity }) catch
|
|
return VaultStoreError.OutOfMemory;
|
|
defer allocator.free(vault_dir);
|
|
|
|
var dir = std.fs.cwd().openDir(vault_dir, .{ .iterate = true }) catch {
|
|
return 0;
|
|
};
|
|
defer dir.close();
|
|
|
|
var count: usize = 0;
|
|
var it = dir.iterate();
|
|
while (try it.next()) |entry| {
|
|
if (entry.kind == .directory) count += 1;
|
|
}
|
|
return count;
|
|
}
|
|
|
|
// ── Internal helpers ─────────────────────────────────────────────────────────
|
|
|
|
/// JSON shape for meta.json parsing.
|
|
const MetaJson = struct {
|
|
version: u64,
|
|
created_ns: i128,
|
|
updated_ns: i128,
|
|
size: usize,
|
|
};
|
|
|
|
fn secretExistsInternal(
|
|
data_dir: []const u8,
|
|
identity: []const u8,
|
|
name: []const u8,
|
|
allocator: std.mem.Allocator,
|
|
) bool {
|
|
const share_path = std.fmt.allocPrint(allocator, "{s}/vaults/{s}/{s}/share.bin", .{ data_dir, identity, name }) catch
|
|
return false;
|
|
defer allocator.free(share_path);
|
|
|
|
std.fs.cwd().access(share_path, .{}) catch return false;
|
|
return true;
|
|
}
|
|
|
|
fn writeSecretInner(
|
|
secret_dir: []const u8,
|
|
data: []const u8,
|
|
version: u64,
|
|
integrity_key: []const u8,
|
|
existing_created_ns: ?i128,
|
|
allocator: std.mem.Allocator,
|
|
) VaultStoreError!void {
|
|
// Create directory
|
|
std.fs.cwd().makePath(secret_dir) catch {
|
|
return VaultStoreError.IoError;
|
|
};
|
|
|
|
// Write share.bin atomically
|
|
const share_path = std.fmt.allocPrint(allocator, "{s}/share.bin", .{secret_dir}) catch
|
|
return VaultStoreError.OutOfMemory;
|
|
defer allocator.free(share_path);
|
|
const share_tmp = std.fmt.allocPrint(allocator, "{s}/share.bin.tmp", .{secret_dir}) catch
|
|
return VaultStoreError.OutOfMemory;
|
|
defer allocator.free(share_tmp);
|
|
|
|
try atomicWrite(share_path, share_tmp, data);
|
|
|
|
// Write checksum.bin atomically
|
|
const checksum = hmac.compute(integrity_key, data);
|
|
const checksum_path = std.fmt.allocPrint(allocator, "{s}/checksum.bin", .{secret_dir}) catch
|
|
return VaultStoreError.OutOfMemory;
|
|
defer allocator.free(checksum_path);
|
|
const checksum_tmp = std.fmt.allocPrint(allocator, "{s}/checksum.bin.tmp", .{secret_dir}) catch
|
|
return VaultStoreError.OutOfMemory;
|
|
defer allocator.free(checksum_tmp);
|
|
|
|
try atomicWrite(checksum_path, checksum_tmp, &checksum);
|
|
|
|
// Write meta.json atomically
|
|
const now = std.time.nanoTimestamp();
|
|
const created_ns = existing_created_ns orelse now;
|
|
|
|
var meta_buf: [512]u8 = undefined;
|
|
const meta_json = std.fmt.bufPrint(&meta_buf,
|
|
\\{{"version":{d},"created_ns":{d},"updated_ns":{d},"size":{d}}}
|
|
, .{ version, created_ns, now, data.len }) catch
|
|
return VaultStoreError.IoError;
|
|
|
|
const meta_path = std.fmt.allocPrint(allocator, "{s}/meta.json", .{secret_dir}) catch
|
|
return VaultStoreError.OutOfMemory;
|
|
defer allocator.free(meta_path);
|
|
const meta_tmp = std.fmt.allocPrint(allocator, "{s}/meta.json.tmp", .{secret_dir}) catch
|
|
return VaultStoreError.OutOfMemory;
|
|
defer allocator.free(meta_tmp);
|
|
|
|
try atomicWrite(meta_path, meta_tmp, meta_json);
|
|
}
|
|
|
|
fn atomicWrite(final_path: []const u8, tmp_path: []const u8, data: []const u8) VaultStoreError!void {
|
|
// Write to temp file
|
|
const tmp_file = std.fs.cwd().createFile(tmp_path, .{}) catch {
|
|
return VaultStoreError.IoError;
|
|
};
|
|
defer tmp_file.close();
|
|
tmp_file.writeAll(data) catch {
|
|
return VaultStoreError.IoError;
|
|
};
|
|
|
|
// Rename atomically
|
|
std.fs.cwd().rename(tmp_path, final_path) catch {
|
|
return VaultStoreError.IoError;
|
|
};
|
|
}
|
|
|
|
// ── Tests ────────────────────────────────────────────────────────────────────
|
|
|
|
test "validateSecretName: valid names" {
|
|
try validateSecretName("default");
|
|
try validateSecretName("my-secret");
|
|
try validateSecretName("secret_123");
|
|
try validateSecretName("a");
|
|
try validateSecretName("ABC-xyz_09");
|
|
}
|
|
|
|
test "validateSecretName: empty name" {
|
|
try std.testing.expectError(VaultStoreError.SecretNameRequired, validateSecretName(""));
|
|
}
|
|
|
|
test "validateSecretName: too long" {
|
|
const long_name = "a" ** (MAX_SECRET_NAME_LEN + 1);
|
|
try std.testing.expectError(VaultStoreError.SecretNameTooLong, validateSecretName(long_name));
|
|
}
|
|
|
|
test "validateSecretName: invalid characters" {
|
|
try std.testing.expectError(VaultStoreError.SecretNameInvalid, validateSecretName("has space"));
|
|
try std.testing.expectError(VaultStoreError.SecretNameInvalid, validateSecretName("has/slash"));
|
|
try std.testing.expectError(VaultStoreError.SecretNameInvalid, validateSecretName("has.dot"));
|
|
try std.testing.expectError(VaultStoreError.SecretNameInvalid, validateSecretName("has@at"));
|
|
}
|
|
|
|
test "writeSecret and readSecret round-trip" {
|
|
const allocator = std.testing.allocator;
|
|
|
|
var tmp_dir_buf: [std.fs.max_path_bytes]u8 = undefined;
|
|
var tmp_dir = std.testing.tmpDir(.{});
|
|
defer tmp_dir.cleanup();
|
|
const tmp_path = try tmp_dir.dir.realpath(".", &tmp_dir_buf);
|
|
|
|
const identity = "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789";
|
|
const name = "my-secret";
|
|
const data = "encrypted share data here";
|
|
const key = "integrity-key-32-bytes-long!!!!";
|
|
|
|
try writeSecret(tmp_path, identity, name, data, 1, key, allocator);
|
|
|
|
const read_data = try readSecret(tmp_path, identity, name, key, allocator);
|
|
defer allocator.free(read_data);
|
|
|
|
try std.testing.expectEqualSlices(u8, data, read_data);
|
|
}
|
|
|
|
test "readSecret: integrity check detects tampering" {
|
|
const allocator = std.testing.allocator;
|
|
|
|
var tmp_dir_buf: [std.fs.max_path_bytes]u8 = undefined;
|
|
var tmp_dir = std.testing.tmpDir(.{});
|
|
defer tmp_dir.cleanup();
|
|
const tmp_path = try tmp_dir.dir.realpath(".", &tmp_dir_buf);
|
|
|
|
const identity = "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef";
|
|
const name = "tamper-test";
|
|
const key = "integrity-key-32-bytes-long!!!!";
|
|
|
|
try writeSecret(tmp_path, identity, name, "original data", 1, key, allocator);
|
|
|
|
// Tamper with the share file
|
|
const share_path = try std.fmt.allocPrint(allocator, "{s}/vaults/{s}/{s}/share.bin", .{ tmp_path, identity, name });
|
|
defer allocator.free(share_path);
|
|
|
|
const file = try std.fs.cwd().openFile(share_path, .{ .mode = .write_only });
|
|
defer file.close();
|
|
try file.writeAll("tampered data!!");
|
|
|
|
try std.testing.expectError(VaultStoreError.IntegrityCheckFailed, readSecret(tmp_path, identity, name, key, allocator));
|
|
}
|
|
|
|
test "readSecret: not found" {
|
|
const allocator = std.testing.allocator;
|
|
|
|
var tmp_dir_buf: [std.fs.max_path_bytes]u8 = undefined;
|
|
var tmp_dir = std.testing.tmpDir(.{});
|
|
defer tmp_dir.cleanup();
|
|
const tmp_path = try tmp_dir.dir.realpath(".", &tmp_dir_buf);
|
|
|
|
try std.testing.expectError(VaultStoreError.NotFound, readSecret(tmp_path, "nonexistent", "nope", "key!", allocator));
|
|
}
|
|
|
|
test "writeSecret: rejects empty identity" {
|
|
const allocator = std.testing.allocator;
|
|
var tmp_dir_buf: [std.fs.max_path_bytes]u8 = undefined;
|
|
var tmp_dir = std.testing.tmpDir(.{});
|
|
defer tmp_dir.cleanup();
|
|
const tmp_path = try tmp_dir.dir.realpath(".", &tmp_dir_buf);
|
|
|
|
try std.testing.expectError(VaultStoreError.IdentityRequired, writeSecret(tmp_path, "", "name", "data", 1, "key!", allocator));
|
|
}
|
|
|
|
test "writeSecret: rejects empty data" {
|
|
const allocator = std.testing.allocator;
|
|
var tmp_dir_buf: [std.fs.max_path_bytes]u8 = undefined;
|
|
var tmp_dir = std.testing.tmpDir(.{});
|
|
defer tmp_dir.cleanup();
|
|
const tmp_path = try tmp_dir.dir.realpath(".", &tmp_dir_buf);
|
|
|
|
try std.testing.expectError(VaultStoreError.SecretDataRequired, writeSecret(tmp_path, "id123", "name", "", 1, "key!", allocator));
|
|
}
|
|
|
|
test "writeSecret: anti-rollback rejects lower version" {
|
|
const allocator = std.testing.allocator;
|
|
|
|
var tmp_dir_buf: [std.fs.max_path_bytes]u8 = undefined;
|
|
var tmp_dir = std.testing.tmpDir(.{});
|
|
defer tmp_dir.cleanup();
|
|
const tmp_path = try tmp_dir.dir.realpath(".", &tmp_dir_buf);
|
|
|
|
const identity = "rollback0123456789abcdef";
|
|
const name = "versioned";
|
|
const key = "integrity-key-32-bytes-long!!!!";
|
|
|
|
try writeSecret(tmp_path, identity, name, "v1 data", 5, key, allocator);
|
|
|
|
// Same version should fail
|
|
try std.testing.expectError(VaultStoreError.VersionConflict, writeSecret(tmp_path, identity, name, "v1 again", 5, key, allocator));
|
|
|
|
// Lower version should fail
|
|
try std.testing.expectError(VaultStoreError.VersionConflict, writeSecret(tmp_path, identity, name, "rollback", 3, key, allocator));
|
|
|
|
// Higher version should succeed
|
|
try writeSecret(tmp_path, identity, name, "v2 data", 6, key, allocator);
|
|
|
|
const read_data = try readSecret(tmp_path, identity, name, key, allocator);
|
|
defer allocator.free(read_data);
|
|
try std.testing.expectEqualSlices(u8, "v2 data", read_data);
|
|
}
|
|
|
|
test "readMeta: returns correct metadata" {
|
|
const allocator = std.testing.allocator;
|
|
|
|
var tmp_dir_buf: [std.fs.max_path_bytes]u8 = undefined;
|
|
var tmp_dir = std.testing.tmpDir(.{});
|
|
defer tmp_dir.cleanup();
|
|
const tmp_path = try tmp_dir.dir.realpath(".", &tmp_dir_buf);
|
|
|
|
const identity = "meta0123456789abcdef";
|
|
const name = "with-meta";
|
|
const key = "integrity-key-32-bytes-long!!!!";
|
|
const data = "some secret data";
|
|
|
|
try writeSecret(tmp_path, identity, name, data, 42, key, allocator);
|
|
|
|
const meta = try readMeta(tmp_path, identity, name, allocator);
|
|
try std.testing.expectEqual(@as(u64, 42), meta.version);
|
|
try std.testing.expectEqual(data.len, meta.size);
|
|
try std.testing.expect(meta.created_ns > 0);
|
|
try std.testing.expect(meta.updated_ns >= meta.created_ns);
|
|
}
|
|
|
|
test "deleteSecret: removes secret" {
|
|
const allocator = std.testing.allocator;
|
|
|
|
var tmp_dir_buf: [std.fs.max_path_bytes]u8 = undefined;
|
|
var tmp_dir = std.testing.tmpDir(.{});
|
|
defer tmp_dir.cleanup();
|
|
const tmp_path = try tmp_dir.dir.realpath(".", &tmp_dir_buf);
|
|
|
|
const identity = "delete0123456789abcdef";
|
|
const name = "to-delete";
|
|
const key = "integrity-key-32-bytes-long!!!!";
|
|
|
|
try writeSecret(tmp_path, identity, name, "doomed", 1, key, allocator);
|
|
|
|
const exists_before = try secretExists(tmp_path, identity, name, allocator);
|
|
try std.testing.expect(exists_before);
|
|
|
|
try deleteSecret(tmp_path, identity, name, allocator);
|
|
|
|
const exists_after = try secretExists(tmp_path, identity, name, allocator);
|
|
try std.testing.expect(!exists_after);
|
|
}
|
|
|
|
test "deleteSecret: not found" {
|
|
const allocator = std.testing.allocator;
|
|
|
|
var tmp_dir_buf: [std.fs.max_path_bytes]u8 = undefined;
|
|
var tmp_dir = std.testing.tmpDir(.{});
|
|
defer tmp_dir.cleanup();
|
|
const tmp_path = try tmp_dir.dir.realpath(".", &tmp_dir_buf);
|
|
|
|
try std.testing.expectError(VaultStoreError.NotFound, deleteSecret(tmp_path, "ghost", "nope", allocator));
|
|
}
|
|
|
|
test "listSecrets: empty for new identity" {
|
|
const allocator = std.testing.allocator;
|
|
|
|
var tmp_dir_buf: [std.fs.max_path_bytes]u8 = undefined;
|
|
var tmp_dir = std.testing.tmpDir(.{});
|
|
defer tmp_dir.cleanup();
|
|
const tmp_path = try tmp_dir.dir.realpath(".", &tmp_dir_buf);
|
|
|
|
const names = try listSecrets(tmp_path, "nobody", allocator);
|
|
defer allocator.free(names);
|
|
|
|
try std.testing.expectEqual(@as(usize, 0), names.len);
|
|
}
|
|
|
|
test "listSecrets: returns all secret names" {
|
|
const allocator = std.testing.allocator;
|
|
|
|
var tmp_dir_buf: [std.fs.max_path_bytes]u8 = undefined;
|
|
var tmp_dir = std.testing.tmpDir(.{});
|
|
defer tmp_dir.cleanup();
|
|
const tmp_path = try tmp_dir.dir.realpath(".", &tmp_dir_buf);
|
|
|
|
const identity = "list0123456789abcdef";
|
|
const key = "integrity-key-32-bytes-long!!!!";
|
|
|
|
try writeSecret(tmp_path, identity, "alpha", "data-a", 1, key, allocator);
|
|
try writeSecret(tmp_path, identity, "beta", "data-b", 1, key, allocator);
|
|
try writeSecret(tmp_path, identity, "gamma", "data-c", 1, key, allocator);
|
|
|
|
const names = try listSecrets(tmp_path, identity, allocator);
|
|
defer {
|
|
for (names) |n| allocator.free(n);
|
|
allocator.free(names);
|
|
}
|
|
|
|
try std.testing.expectEqual(@as(usize, 3), names.len);
|
|
|
|
// Check all three names are present (order is not guaranteed)
|
|
var found_alpha = false;
|
|
var found_beta = false;
|
|
var found_gamma = false;
|
|
for (names) |n| {
|
|
if (std.mem.eql(u8, n, "alpha")) found_alpha = true;
|
|
if (std.mem.eql(u8, n, "beta")) found_beta = true;
|
|
if (std.mem.eql(u8, n, "gamma")) found_gamma = true;
|
|
}
|
|
try std.testing.expect(found_alpha);
|
|
try std.testing.expect(found_beta);
|
|
try std.testing.expect(found_gamma);
|
|
}
|
|
|
|
test "countSecrets: counts correctly" {
|
|
const allocator = std.testing.allocator;
|
|
|
|
var tmp_dir_buf: [std.fs.max_path_bytes]u8 = undefined;
|
|
var tmp_dir = std.testing.tmpDir(.{});
|
|
defer tmp_dir.cleanup();
|
|
const tmp_path = try tmp_dir.dir.realpath(".", &tmp_dir_buf);
|
|
|
|
const identity = "count0123456789abcdef";
|
|
const key = "integrity-key-32-bytes-long!!!!";
|
|
|
|
try std.testing.expectEqual(@as(usize, 0), try countSecrets(tmp_path, identity, allocator));
|
|
|
|
try writeSecret(tmp_path, identity, "first", "data-1", 1, key, allocator);
|
|
try std.testing.expectEqual(@as(usize, 1), try countSecrets(tmp_path, identity, allocator));
|
|
|
|
try writeSecret(tmp_path, identity, "second", "data-2", 1, key, allocator);
|
|
try std.testing.expectEqual(@as(usize, 2), try countSecrets(tmp_path, identity, allocator));
|
|
}
|
|
|
|
test "writeSecret: update preserves created_ns" {
|
|
const allocator = std.testing.allocator;
|
|
|
|
var tmp_dir_buf: [std.fs.max_path_bytes]u8 = undefined;
|
|
var tmp_dir = std.testing.tmpDir(.{});
|
|
defer tmp_dir.cleanup();
|
|
const tmp_path = try tmp_dir.dir.realpath(".", &tmp_dir_buf);
|
|
|
|
const identity = "preserve0123456789abcdef";
|
|
const name = "preserve-ts";
|
|
const key = "integrity-key-32-bytes-long!!!!";
|
|
|
|
try writeSecret(tmp_path, identity, name, "v1", 1, key, allocator);
|
|
const meta1 = try readMeta(tmp_path, identity, name, allocator);
|
|
|
|
// Update with higher version
|
|
try writeSecret(tmp_path, identity, name, "v2", 2, key, allocator);
|
|
const meta2 = try readMeta(tmp_path, identity, name, allocator);
|
|
|
|
// created_ns should be preserved
|
|
try std.testing.expectEqual(meta1.created_ns, meta2.created_ns);
|
|
// updated_ns should be >= the first
|
|
try std.testing.expect(meta2.updated_ns >= meta1.updated_ns);
|
|
// version should be updated
|
|
try std.testing.expectEqual(@as(u64, 2), meta2.version);
|
|
}
|
|
|
|
test "secretExists: false for missing, true for existing" {
|
|
const allocator = std.testing.allocator;
|
|
|
|
var tmp_dir_buf: [std.fs.max_path_bytes]u8 = undefined;
|
|
var tmp_dir = std.testing.tmpDir(.{});
|
|
defer tmp_dir.cleanup();
|
|
const tmp_path = try tmp_dir.dir.realpath(".", &tmp_dir_buf);
|
|
|
|
const identity = "exists0123456789abcdef";
|
|
const key = "integrity-key-32-bytes-long!!!!";
|
|
|
|
try std.testing.expect(!try secretExists(tmp_path, identity, "nope", allocator));
|
|
|
|
try writeSecret(tmp_path, identity, "yes", "data", 1, key, allocator);
|
|
try std.testing.expect(try secretExists(tmp_path, identity, "yes", allocator));
|
|
}
|