Files
Abyss/AbyssCli/Program.cs
2025-08-24 00:56:08 +08:00

365 lines
11 KiB
C#

// Program.cs
using System;
using System.Net.Http;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
using System.Collections.Generic;
using System.Text.Json.Serialization.Metadata;
using NSec.Cryptography;
public class ChallengeRequestBody
{
public string Response { get; set; } = "";
}
public class CreateRequestBody
{
public string Response { get; set; } = "";
public string Name { get; set; } = "";
public string Parent { get; set; } = "";
public int Privilege { get; set; }
public string PublicKey { get; set; } = "";
}
public static class Ed25519Utils
{
public static (string privateBase64, string publicBase64) GenerateKeyPairBase64()
{
var algorithm = SignatureAlgorithm.Ed25519;
var creationParameters = new KeyCreationParameters
{
ExportPolicy = KeyExportPolicies.AllowPlaintextExport
};
using var key = Key.Create(algorithm, creationParameters);
var priv = key.Export(KeyBlobFormat.RawPrivateKey);
var pub = key.Export(KeyBlobFormat.RawPublicKey);
return (Convert.ToBase64String(priv), Convert.ToBase64String(pub));
}
public static string SignBase64PrivateKey(string privateKeyBase64, byte[] dataToSign)
{
var algorithm = SignatureAlgorithm.Ed25519;
var privateBytes = Convert.FromBase64String(privateKeyBase64);
using var key = Key.Import(algorithm, privateBytes, KeyBlobFormat.RawPrivateKey);
var sig = algorithm.Sign(key, dataToSign);
return Convert.ToBase64String(sig);
}
}
public static class Program
{
static async Task<int> Main(string[] args)
{
if (args == null || args.Length == 0)
{
PrintUsage();
return 1;
}
var cmd = args[0].ToLowerInvariant();
try
{
switch (cmd)
{
case "open":
return await CmdOpen(args);
case "destroy":
return await CmdDestroy(args);
case "valid":
return await CmdValid(args);
case "create":
return await CmdCreate(args);
default:
Console.Error.WriteLine("Unknown command.");
PrintUsage();
return 2;
}
}
catch (Exception ex)
{
Console.Error.WriteLine($"Error: {ex.Message}");
return 3;
}
}
static void PrintUsage()
{
Console.WriteLine("Usage:");
Console.WriteLine(" AbyssCli open <baseUrl> <user> <privateKeyBase64>");
Console.WriteLine(" AbyssCli destroy <baseUrl> <token>");
Console.WriteLine(" AbyssCli valid <baseUrl> <token>");
Console.WriteLine(" AbyssCli create <baseUrl> <user> <privateKeyBase64> <newUsername> <privilege>");
}
static HttpClient CreateHttpClient(string baseUrl)
{
var client = new HttpClient();
client.BaseAddress = new Uri(baseUrl.TrimEnd('/') + "/");
return client;
}
static async Task<int> CmdOpen(string[] args)
{
if (args.Length != 4)
{
Console.Error.WriteLine("open requires 3 arguments: <baseUrl> <user> <privateKeyBase64>");
return 1;
}
var baseUrl = args[1];
var user = args[2];
var privateKeyBase64 = args[3];
using var client = CreateHttpClient(baseUrl);
// 1. GET challenge
var challenge = await GetChallenge(client, user);
if (challenge == null)
{
Console.Error.WriteLine("Failed to get challenge.");
return 1;
}
// 2. Sign challenge (challenge is base64 string)
byte[] challengeBytes;
try
{
challengeBytes = Convert.FromBase64String(challenge);
}
catch
{
Console.Error.WriteLine("Challenge is not valid base64.");
return 1;
}
string signatureBase64;
try
{
signatureBase64 = Ed25519Utils.SignBase64PrivateKey(privateKeyBase64, challengeBytes);
}
catch (Exception ex)
{
Console.Error.WriteLine($"Signing failed: {ex.Message}");
return 1;
}
// 3. POST response to get token
var token = await PostResponseForToken(client, user, signatureBase64);
if (token == null)
{
Console.Error.WriteLine("Authentication failed or server returned no token.");
return 1;
}
Console.WriteLine(token);
return 0;
}
static async Task<int> CmdDestroy(string[] args)
{
if (args.Length != 3)
{
Console.Error.WriteLine("destroy requires 2 arguments: <baseUrl> <token>");
return 1;
}
var baseUrl = args[1];
var token = args[2];
using var client = CreateHttpClient(baseUrl);
var resp = await client.PostAsync($"api/user/destroy?token={token}", null);
if (!resp.IsSuccessStatusCode)
{
Console.Error.WriteLine($"Destroy failed: {resp.StatusCode}");
var txt = await TryReadResponseText(resp);
if (!string.IsNullOrEmpty(txt)) Console.Error.WriteLine(txt);
return 1;
}
var body = await resp.Content.ReadAsStringAsync();
Console.WriteLine("Success");
return 0;
}
static async Task<int> CmdValid(string[] args)
{
if (args.Length != 3)
{
Console.Error.WriteLine("valid requires 2 arguments: <baseUrl> <token>");
return 1;
}
var baseUrl = args[1];
var token = args[2];
using var client = CreateHttpClient(baseUrl);
var resp = await client.PostAsync($"api/user/validate?token={token}", null);
if (!resp.IsSuccessStatusCode)
{
Console.WriteLine("Invalid");
return 1;
}
var content = await resp.Content.ReadAsStringAsync();
// server likely returns JSON string (e.g. "username"), try to parse JSON string
try
{
var username = JsonSerializer.Deserialize<string>(content, jsonOptions);
if (username == null)
{
Console.WriteLine("Invalid");
return 1;
}
Console.WriteLine(username);
return 0;
}
catch
{
// fallback
Console.WriteLine(content.Trim('"'));
return 0;
}
}
static async Task<int> CmdCreate(string[] args)
{
if (args.Length != 6)
{
Console.Error.WriteLine("create requires 5 arguments: <baseUrl> <user> <privateKeyBase64> <newUsername> <privilege>");
return 1;
}
var baseUrl = args[1];
var user = args[2];
var privateKeyBase64 = args[3];
var newUsername = args[4];
if (!int.TryParse(args[5], out var privilege))
{
Console.Error.WriteLine("Privilege must be an integer.");
return 1;
}
using var client = CreateHttpClient(baseUrl);
// 1. Get challenge for creator user
var challenge = await GetChallenge(client, user);
if (challenge == null)
{
Console.Error.WriteLine("Failed to get challenge for creator.");
return 1;
}
byte[] challengeBytes;
try
{
challengeBytes = Convert.FromBase64String(challenge);
}
catch
{
Console.Error.WriteLine("Challenge is not valid base64.");
return 1;
}
string signatureBase64;
try
{
signatureBase64 = Ed25519Utils.SignBase64PrivateKey(privateKeyBase64, challengeBytes);
}
catch (Exception ex)
{
Console.Error.WriteLine($"Signing failed: {ex.Message}");
return 1;
}
// 2. Generate key pair for new user
var (newPrivBase64, newPubBase64) = Ed25519Utils.GenerateKeyPairBase64();
// 3. Build create payload
var payload = new CreateRequestBody
{
Response = signatureBase64,
Name = newUsername,
Parent = user,
Privilege = privilege,
PublicKey = newPubBase64
};
var json = JsonSerializer.Serialize(payload, jsonOptions);
var request = new HttpRequestMessage(new HttpMethod("PATCH"), $"api/user/{Uri.EscapeDataString(user)}")
{
Content = new StringContent(json, Encoding.UTF8, "application/json")
};
var resp = await client.SendAsync(request);
var respText = await TryReadResponseText(resp);
if (!resp.IsSuccessStatusCode)
{
Console.Error.WriteLine($"Create failed: {resp.StatusCode}");
if (!string.IsNullOrEmpty(respText)) Console.Error.WriteLine(respText);
return 1;
}
Console.WriteLine("Success");
Console.WriteLine("NewUserPrivateKeyBase64:");
Console.WriteLine(newPrivBase64);
Console.WriteLine("NewUserPublicKeyBase64:");
Console.WriteLine(newPubBase64);
return 0;
}
static async Task<string?> GetChallenge(HttpClient client, string user)
{
var resp = await client.GetAsync($"api/user/{Uri.EscapeDataString(user)}");
if (!resp.IsSuccessStatusCode) return null;
var content = await resp.Content.ReadAsStringAsync();
// server probably returns JSON string; try to deserialize to string
try
{
var s = JsonSerializer.Deserialize<string>(content, jsonOptions);
if (s != null) return s;
}
catch { /* ignore */ }
// fallback: trim quotes
return content.Trim('"');
}
static async Task<string?> PostResponseForToken(HttpClient client, string user, string signatureBase64)
{
var body = new ChallengeRequestBody { Response = signatureBase64 };
var json = JsonSerializer.Serialize(body, jsonOptions);
var resp = await client.PostAsync($"api/user/{Uri.EscapeDataString(user)}",
new StringContent(json, Encoding.UTF8, "application/json"));
if (!resp.IsSuccessStatusCode) return null;
var content = await resp.Content.ReadAsStringAsync();
try
{
var token = JsonSerializer.Deserialize<string>(content, jsonOptions);
if (!string.IsNullOrEmpty(token)) return token;
}
catch { /* ignore */ }
return content.Trim('"');
}
static async Task<string> TryReadResponseText(HttpResponseMessage resp)
{
try
{
return await resp.Content.ReadAsStringAsync();
}
catch
{
return "";
}
}
static readonly JsonSerializerOptions jsonOptions = new()
{
TypeInfoResolver = new DefaultJsonTypeInfoResolver()
};
}