initial commit

Signed-off-by: sisungo <[email protected]>
This commit is contained in:
2026-07-18 20:30:39 +08:00
commit bfcc98b50e
39 changed files with 2904 additions and 0 deletions
+16
View File
@@ -0,0 +1,16 @@
[package]
name = "semios_account"
version = "0.1.0"
edition = "2024"
[features]
default = ["client"]
client = []
[dependencies]
cfg-if = "1"
bitflags = { version = "2", features = ["serde"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
thiserror = "2"
uuid = { version = "1", features = ["serde"] }
+161
View File
@@ -0,0 +1,161 @@
//! User authentication.
use bitflags::bitflags;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use uuid::Uuid;
macro_rules! decl_session_args {
(pub struct $sn:ident { $(pub $n:ident : $t:ty,)* }) => {
#[derive(Debug, Clone)]
pub struct $sn {
$(pub $n: $t),*
}
impl $sn {
pub fn parse(args: &[String]) -> Result<Self, Box<dyn std::error::Error>> {
Self::_from_session_args(SessionArgs::parse(args)?)
}
pub fn compose(&self) -> Vec<String> {
self._to_session_args().compose()
}
fn _from_session_args(sa: SessionArgs) -> Result<Self, Box<dyn std::error::Error>> {
Ok(Self {
$(
$n: sa.0.get(stringify!($n))
.ok_or_else(|| Box::<dyn std::error::Error>::from(format!(
"argument {} not found", stringify!($n),
)))?
.parse::<$t>()?
),*
})
}
fn _to_session_args(&self) -> SessionArgs {
let mut map = HashMap::new();
$(
map.insert(stringify!($n).into(), self.$n.to_string());
)*
SessionArgs(map)
}
}
};
}
/// Maximum count of auth methods enabled for one user.
pub const USER_MAX_AUTH_METHODS: usize = 32;
/// Record of an auth method enabled for a user.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UserAuthMethodRecord {
/// Name of the auth method.
pub name: String,
/// Flags of the auth method.
pub flags: UserAuthMethodFlags,
}
bitflags! {
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub struct UserAuthMethodFlags: u32 {
/// Indicates if the auth method is a "main" auth method.
///
/// A "main" auth method is unique for a user. It provides a master key for secret storage.
const MAIN = 1;
}
}
/// Information about an authentication method.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AuthMethodInfo {
/// Name of the auth method.
pub name: String,
/// True if the auth method supports providing the master key.
pub provides_master_key: bool,
/// Supported operation modes.
pub modes: Vec<ModeInfo>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ModeInfo {
/// Name of the operation mode.
pub name: String,
}
/// Command line of an authentication method provider.
#[derive(Debug, Clone)]
pub enum AuthCli {
/// Queries information about the auth method.
///
/// The implementor should output an [`AuthMethod`] in JSON to stdout stream, and then exit.
QueryInformation,
/// Begins an authentication session.
Authenticate(AuthenticateArgs),
/// Begins an update session.
Update(UpdateArgs),
}
impl AuthCli {
pub fn parse(args: &[String]) -> Result<Self, Box<dyn std::error::Error>> {
match args.first().map(String::as_str) {
Some("QueryInformation") => Ok(Self::QueryInformation),
Some("Authenticate") => Ok(Self::Authenticate(AuthenticateArgs::parse(&args[1..])?)),
Some("Update") => Ok(Self::Update(UpdateArgs::parse(&args[1..])?)),
Some(unexpected) => Err(Box::from(format!("unexpected command `{unexpected}`"))),
None => Err(Box::from("no command specified")),
}
}
pub fn compose(&self) -> Vec<String> {
match self {
Self::QueryInformation => vec!["QueryInformation".into()],
Self::Authenticate(args) => {
let mut ret = vec!["Authenticate".into()];
ret.append(&mut args.compose());
ret
}
Self::Update(args) => {
let mut ret = vec!["Update".into()];
ret.append(&mut args.compose());
ret
}
}
}
}
decl_session_args! {
pub struct AuthenticateArgs {
pub user_uuid: Uuid,
pub mode: String,
}
}
decl_session_args! {
pub struct UpdateArgs {
pub user_uuid: Uuid,
pub mode: String,
}
}
#[derive(Debug, Clone)]
struct SessionArgs(HashMap<String, String>);
impl SessionArgs {
fn parse(args: &[String]) -> Result<Self, Box<dyn std::error::Error>> {
let mut map = HashMap::with_capacity(args.len());
for arg in args {
let Some((key, val)) = arg.split_once('=') else {
return Err(Box::from("invalid argument format"));
};
map.insert(key.into(), val.into());
}
Ok(Self(map))
}
fn compose(&self) -> Vec<String> {
self.0.iter().map(|(k, v)| format!("{k}={v}")).collect()
}
}
+95
View File
@@ -0,0 +1,95 @@
use crate::{
auth::UserAuthMethodRecord,
error::Error,
protocol::*,
raw_client::Connection,
record::{GroupInfo, UserInfo},
secret::Secret,
};
use serde::{Serialize, de::DeserializeOwned};
use std::path::PathBuf;
#[derive(Debug)]
pub struct Client {
conn: Connection,
buf: Vec<u8>,
}
impl Client {
pub fn connect_default() -> std::io::Result<Self> {
Self::connect(&crate::protocol::uri())
}
pub fn connect(uri: &str) -> std::io::Result<Self> {
Ok(Self {
conn: Connection::connect(uri)?,
buf: Vec::with_capacity(512),
})
}
pub fn invoke<P: Serialize, R: DeserializeOwned>(
&mut self,
method: String,
params: P,
) -> Result<R, Error> {
self.buf.clear();
let params =
serde_json::to_value(params).map_err(|e| Error::Communication(e.to_string()))?;
let req = Request { method, params };
serde_json::to_writer(&mut self.buf, &req)
.map_err(|e| Error::Communication(e.to_string()))?;
self.conn
.send(&self.buf)
.map_err(|e| Error::Communication(e.to_string()))?;
self.conn
.recv(&mut self.buf)
.map_err(|e| Error::Communication(e.to_string()))?;
let resp: Response =
serde_json::from_slice(&self.buf).map_err(|e| Error::Communication(e.to_string()))?;
resp.into_result()
.map(|x| serde_json::from_value(x).map_err(|e| Error::Communication(e.to_string())))
.flatten()
}
pub fn get_user_info(&mut self, args: GetUserInfoArgs) -> Result<UserInfo, Error> {
self.invoke(GET_USER_INFO.into(), args)
}
pub fn get_group_info(&mut self, args: GetGroupInfoArgs) -> Result<GroupInfo, Error> {
self.invoke(GET_GROUP_INFO.into(), args)
}
pub fn get_secret(&mut self, args: GetSecretArgs) -> Result<Secret, Error> {
self.invoke(GET_SECRET.into(), args)
}
pub fn set_secret(&mut self, args: SetSecretArgs) -> Result<(), Error> {
self.invoke(SET_SECRET.into(), args)
}
pub fn provide_master_key(&mut self, args: ProvideMasterKeyArgs) -> Result<(), Error> {
self.invoke(PROVIDE_MASTER_KEY.into(), args)
}
pub fn clear_master_key(&mut self, args: ClearMasterKeyArgs) -> Result<(), Error> {
self.invoke(CLEAR_MASTER_KEY.into(), args)
}
pub fn get_user_auth_methods(
&mut self,
args: GetUserAuthMethodsArgs,
) -> Result<Vec<UserAuthMethodRecord>, Error> {
self.invoke(GET_USER_AUTH_METHODS.into(), args)
}
pub fn get_auth_method_path(&mut self, args: GetAuthMethodPathArgs) -> Result<PathBuf, Error> {
self.invoke(GET_AUTH_METHOD_PATH.into(), args)
}
pub fn user_add_auth_method(&mut self, args: UserAddAuthMethodArgs) -> Result<(), Error> {
self.invoke(USER_ADD_AUTH_METHOD.into(), args)
}
pub fn create_user(&mut self, args: CreateUserArgs) -> Result<(), Error> {
self.invoke(CREATE_USER.into(), args)
}
}
+59
View File
@@ -0,0 +1,59 @@
//! Errors.
use serde::{Deserialize, Serialize};
use std::fmt::Display;
/// An error.
#[derive(Debug, Clone, Serialize, Deserialize, thiserror::Error)]
pub enum Error {
/// Communication error.
#[error("communication error: {0}")]
Communication(String),
/// The specified method is not implemented.
#[error("`{0}`: Not implemented")]
NotImplemented(String),
/// Parameters are not suitable for the called method.
#[error("invalid parameters: {0}")]
InvalidParams(String),
/// The user has not enough permission to perform the action.
#[error("permission denied")]
PermissionDenied,
/// The operation operates on a user, but the specified user does not exist.
#[error("no such user")]
NoSuchUser,
/// The operation operates on a group, but the specified group does not exist.
#[error("no such group")]
NoSuchGroup,
/// The operation operates on a secret, but the specified secret does not exist.
#[error("no such secret")]
NoSuchSecret,
/// Attempted to launch an auth method that is not installed on current system.
#[error("no such auth method")]
NoSuchAuthMethod,
/// Attempted to authenticate/update authentication via an auth method that is not activated for the user.
///
/// **NOTE**: This does not imply that the auth method is installed on current system.
#[error("the specified auth method is not activated for the user")]
NotActivatedAuthMethod,
/// TODO
#[error("already exists")]
AlreadyExists,
/// An internal error.
#[error("internal error: {0}")]
Internal(String),
}
impl Error {
pub fn make_internal(x: impl Display) -> Self {
Self::Internal(x.to_string())
}
}
+19
View File
@@ -0,0 +1,19 @@
pub mod auth;
pub mod error;
pub mod protocol;
pub mod record;
pub mod secret;
pub mod wellknown;
#[cfg(feature = "client")]
cfg_if::cfg_if! {
if #[cfg(target_family = "unix")] {
#[path = "raw_client_unix.rs"]
pub mod raw_client;
} else {
std::compile_error!("target not supported");
}
}
#[cfg(feature = "client")]
pub mod client;
+266
View File
@@ -0,0 +1,266 @@
use crate::{
error::Error,
record::{GroupInfo, HostGid, HostUid, SecureTag},
secret::{MasterKeyProvision, Secret},
};
use cfg_if::cfg_if;
use serde::{Deserialize, Serialize, de::DeserializeOwned};
use std::collections::{HashMap, HashSet};
use uuid::Uuid;
/// Max length of a single message.
pub const MAX_MESSAGE_LEN: usize = 128 * 1024;
// ==- IPC URI -==
cfg_if! {
if #[cfg(target_os = "linux")] {
pub const DEFAULT_URI: &str = "unix://@verified:org.semilabs.os/accountd";
} else if #[cfg(target_family = "unix")] {
pub const DEFAULT_URI: &str = "unix:///var/run/accountd.sock";
} else {
pub const DEFAULT_URI: &str = "";
}
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub enum Uri {
Unix(std::path::PathBuf),
UnixAbstract(String),
}
impl std::str::FromStr for Uri {
type Err = std::io::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if let Some(unix) = s.strip_prefix("unix://") {
if unix.starts_with('/') {
Ok(Self::Unix(unix.into()))
} else if unix.starts_with('@') {
Ok(Self::UnixAbstract(unix[1..].into()))
} else {
Err(std::io::ErrorKind::AddrNotAvailable.into())
}
} else {
Err(std::io::ErrorKind::AddrNotAvailable.into())
}
}
}
/// Gets IPC URI.
pub fn uri() -> String {
std::env::var("ACCOUNT_IPC_URI").unwrap_or_else(|_| DEFAULT_URI.into())
}
// ==- Basic Definitions -==
/// A request.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Request {
pub method: String,
pub params: serde_json::Value,
}
impl Request {
pub fn params<T: DeserializeOwned>(self) -> Result<T, Error> {
serde_json::from_value(self.params).map_err(|e| Error::InvalidParams(e.to_string()))
}
}
/// A response.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Response {
pub success: bool,
pub value: serde_json::Value,
}
impl Response {
pub fn from_result<R: Serialize>(result: Result<R, Error>) -> Self {
let success = result.is_ok();
let value = match result {
Ok(x) => serde_json::to_value(x).expect("response not serialized"),
Err(e) => serde_json::to_value(e).expect("response not serialized"),
};
Self { success, value }
}
pub fn into_result<R: DeserializeOwned>(self) -> Result<R, Error> {
if self.success {
Ok(serde_json::from_value(self.value)
.map_err(|x| Error::InvalidParams(x.to_string()))?)
} else {
Err(serde_json::from_value(self.value)
.map_err(|x| Error::InvalidParams(x.to_string()))?)
}
}
}
// ==- Method Names: Querying Server Information -==
pub const SERVER_VERSION: &str = "ServerVersion";
// ==- Methods: Querying Records -==
pub const LIST_USER: &str = "ListUser";
pub const LIST_GROUP: &str = "ListGroup";
pub const GET_USER_INFO: &str = "GetUserInfo";
pub const GET_GROUP_INFO: &str = "GetGroupInfo";
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ListUserArgs {
pub start: u32,
pub len: u32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ListGroupArgs {
pub start: u32,
pub len: u32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum GetUserInfoArgs {
Uuid(Uuid),
Username(String),
HostUid(HostUid),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum GetGroupInfoArgs {
Uuid(Uuid),
Groupname(String),
HostGid(HostGid),
}
// ==- Methods: Managed Secrets -==
pub const GET_SECRET: &str = "GetSecret";
pub const SET_SECRET: &str = "SetSecret";
pub const REMOVE_SECRET: &str = "RemoveSecret";
pub const PROVIDE_MASTER_KEY: &str = "ProvideMasterKey";
pub const CLEAR_MASTER_KEY: &str = "ClearMasterKey";
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GetSecretArgs {
pub user: Uuid,
pub name: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SetSecretArgs {
pub user: Uuid,
pub secret: Secret,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RemoveSecretArgs {
pub user: Uuid,
pub name: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProvideMasterKeyArgs {
pub user: Uuid,
pub provision: MasterKeyProvision,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ClearMasterKeyArgs {
pub user: Uuid,
}
// ==- Methods: User Management -==
pub const CREATE_USER: &str = "CreateUser";
pub const REMOVE_USER: &str = "RemoveUser";
pub const CREATE_GROUP: &str = "CreateGroup";
pub const REMOVE_GROUP: &str = "RemoveGroup";
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CreateUserArgs {
/// User name.
pub username: String,
/// Full name.
pub fullname: Option<String>,
/// Specify a host UID.
pub host_uid: Option<HostUid>,
/// User description.
#[serde(default)]
pub description: String,
/// Secure tags.
#[serde(default)]
pub secure_tags: HashSet<SecureTag>,
/// Extra records.
#[serde(default)]
pub extra_records: HashMap<String, String>,
/// Defaults of the user.
#[serde(default)]
pub defaults: HashMap<String, String>,
/// Home directory of the user.
pub home_directory: Option<String>,
/// Initial groups of the newly created user.
#[serde(default)]
pub groups: Vec<Uuid>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RemoveUserArgs {
/// UUID of the user to be removed.
pub uuid: Uuid,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CreateGroupArgs {
/// Group name.
pub groupname: String,
/// Full group name.
pub fullname: Option<String>,
/// Group ID on current host.
pub host_gid: Option<HostGid>,
/// Group description.
#[serde(default)]
pub description: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RemoveGroupArgs {
/// UUID of the group to be removed.
pub uuid: Uuid,
}
// ==- Methods: Authentication -==
pub const GET_USER_AUTH_METHODS: &str = "GetUserAuthMethods";
pub const GET_AUTH_METHOD_PATH: &str = "GetAuthMethodPath";
pub const USER_ADD_AUTH_METHOD: &str = "UserAddAuthMethod";
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GetUserAuthMethodsArgs {
/// UUID of the user.
pub user: Uuid,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GetAuthMethodPathArgs {
/// Name of the auth method.
pub name: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UserAddAuthMethodArgs {
/// UUID of the user.
pub user: Uuid,
/// Name of the auth method.
pub auth_method: String,
}
+49
View File
@@ -0,0 +1,49 @@
use crate::protocol::{MAX_MESSAGE_LEN, Uri};
use cfg_if::cfg_if;
use std::{
io::{Read, Write},
os::unix::net::{SocketAddr, UnixStream},
};
#[derive(Debug)]
pub struct Connection(UnixStream);
impl Connection {
pub fn connect(uri: &str) -> std::io::Result<Self> {
let uri: Uri = uri.parse()?;
let sockaddr = match uri {
Uri::Unix(path) => SocketAddr::from_pathname(path)?,
Uri::UnixAbstract(_name) => {
cfg_if! {
if #[cfg(target_os = "linux")] {
use std::os::linux::net::SocketAddrExt;
SocketAddr::from_abstract_name(_name.as_bytes())?
} else {
return Err(std::io::ErrorKind::Unsupported.into());
}
}
}
};
Ok(Self(UnixStream::connect_addr(&sockaddr)?))
}
pub fn send(&mut self, data: &[u8]) -> std::io::Result<()> {
if data.len() > MAX_MESSAGE_LEN {
return Err(std::io::ErrorKind::FileTooLarge.into());
}
self.0.write_all(&(data.len() as u32).to_le_bytes())?;
self.0.write(data)?;
Ok(())
}
pub fn recv(&mut self, buf: &mut Vec<u8>) -> std::io::Result<()> {
let mut len = [0u8; size_of::<u32>()];
self.0.read_exact(&mut len)?;
let len = u32::from_le_bytes(len) as usize;
if len > MAX_MESSAGE_LEN {
return Err(std::io::ErrorKind::FileTooLarge.into());
}
buf.resize(len, 0);
self.0.read_exact(buf)?;
Ok(())
}
}
+107
View File
@@ -0,0 +1,107 @@
//! Basic user information records.
use cfg_if::cfg_if;
use serde::{Deserialize, Serialize};
use std::collections::{HashMap, HashSet};
use uuid::Uuid;
cfg_if! {
if #[cfg(target_family = "unix")] {
pub type HostUid = u32;
pub type HostGid = u32;
} else if #[cfg(target_family = "windows")] {
pub type HostUid = String;
pub type HostGid = String;
} else {
pub type HostUid = u64;
pub type HostGid = u64;
}
}
/// Basical user information.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UserInfo {
/// User UUID.
pub uuid: Uuid,
/// Username.
pub username: String,
/// User ID on current host.
pub host_uid: Option<HostUid>,
/// Full name.
pub fullname: String,
/// User description.
pub description: String,
/// Secure tags.
pub secure_tags: HashSet<SecureTag>,
/// Extra records.
pub extra_records: HashMap<String, String>,
/// Defaults of the user.
pub defaults: HashMap<String, String>,
/// Home directory of the user.
pub home_directory: Option<String>,
/// Time the user is created.
pub creation_time: i64,
/// Time the user is last logged in.
pub last_login_time: i64,
}
impl UserInfo {
pub fn clear_host_specific(&mut self) {
self.host_uid = None;
}
}
/// User secure tag.
#[derive(Debug, Clone, Serialize, Deserialize, Hash, PartialEq, Eq)]
pub enum SecureTag {
/// Indicates that the user can be logged in.
Login,
/// Indicates that the user may create another user.
CreateUser,
/// Indicates that the user may remove users.
RemoveUser,
/// Read secrets of other users.
ReadSecret,
/// Set or delete secrets of other users.
WriteSecret,
}
/// Basical group information.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GroupInfo {
/// Group UUID.
pub uuid: Uuid,
/// Group name.
pub groupname: String,
/// Group ID on current host.
pub host_gid: Option<HostGid>,
/// Full group name.
pub fullname: String,
/// Group description.
pub description: String,
/// Time the group is created.
pub creation_time: i64,
}
impl GroupInfo {
pub fn clear_host_specific(&mut self) {
self.host_gid = None;
}
}
+53
View File
@@ -0,0 +1,53 @@
//! User managed secrets.
//!
//! Managed Secrets is a feature that allows applications host their secrets (e.g. password database keys) here,
//! and get the secrets later with proper authentication, like Apple Keychain and KDE KWallet.
use bitflags::bitflags;
use serde::{Deserialize, Serialize};
/// Maximum length of a secret name.
pub const SECRET_NAME_LEN: usize = 1024;
/// Maximum length of a managed secret item.
pub const SECRET_MAX_LEN: usize = 4096;
/// Maximum count of managed secrets owned by a user.
pub const MAX_SECRET_COUNT: usize = 16384;
/// A managed secret item.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Secret {
/// Name of the secret.
pub name: String,
/// Data of the secret.
pub data: Vec<u8>,
/// Security flags of the secret.
pub security_flags: SecurityFlags,
/// Time the secret is created.
pub creation_time: i64,
/// Time the secret will be expired.
pub expiration_time: i64,
}
bitflags! {
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
pub struct SecurityFlags: u32 {
const ENCRYPTED = 1;
const WRITE_PROTECTED = 2;
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MasterKeyProvision {
pub algorithm: String,
pub key: Vec<u8>,
pub expiration_time: i64,
}
impl MasterKeyProvision {
pub const ALGORITHM_AES_256_GCM: &str = "AES-256-GCM";
}
+19
View File
@@ -0,0 +1,19 @@
//! "Well-known" strings.
/// Command-line login shell. Used in "defaults" field of a user.
pub const CLI_LOGIN_SHELL: &str = "CliLoginShell";
/// Birth date. Used in "extra_records" field of a user.
pub const BIRTH_DATE: &str = "BirthDate";
/// Gender. Used in "extra_records" field of a user.
pub const GENDER: &str = "Gender";
/// Name of the GUI interactive operation mode.
pub const OP_MODE_GUI: &str = "GUI";
/// Name of the CLI interactive operation mode.
pub const OP_MODE_CLI: &str = "CLI";
/// Name of the API non-interactive operation mode.
pub const OP_MODE_API: &str = "API";