clawdentity_core/identity/
identity.rs1use std::fs;
2use std::path::{Path, PathBuf};
3
4use base64::Engine;
5use base64::engine::general_purpose::URL_SAFE_NO_PAD;
6use ed25519_dalek::{SigningKey, VerifyingKey};
7use getrandom::fill as getrandom_fill;
8use serde::{Deserialize, Serialize};
9
10use crate::config::{CliConfig, ConfigPathOptions, get_config_dir, resolve_config, write_config};
11use crate::did::{did_authority_from_url, new_human_did};
12use crate::error::{CoreError, Result};
13
14const IDENTITY_FILE: &str = "identity.json";
15const FILE_MODE: u32 = 0o600;
16
17#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
18#[serde(rename_all = "camelCase")]
19pub struct LocalIdentity {
20 pub did: String,
21 pub public_key: String,
22 pub secret_key: String,
23 pub registry_url: String,
24}
25
26#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
27#[serde(rename_all = "camelCase")]
28pub struct PublicIdentityView {
29 pub did: String,
30 pub public_key: String,
31 pub registry_url: String,
32}
33
34impl LocalIdentity {
35 pub fn public_view(&self) -> PublicIdentityView {
37 PublicIdentityView {
38 did: self.did.clone(),
39 public_key: self.public_key.clone(),
40 registry_url: self.registry_url.clone(),
41 }
42 }
43}
44
45fn identity_path(options: &ConfigPathOptions) -> Result<PathBuf> {
46 Ok(get_config_dir(options)?.join(IDENTITY_FILE))
47}
48
49fn set_secure_permissions(path: &Path) -> Result<()> {
50 #[cfg(unix)]
51 {
52 use std::os::unix::fs::PermissionsExt;
53 let perms = fs::Permissions::from_mode(FILE_MODE);
54 fs::set_permissions(path, perms).map_err(|source| CoreError::Io {
55 path: path.to_path_buf(),
56 source,
57 })?;
58 }
59 Ok(())
60}
61
62fn write_secure_json<T: Serialize>(path: &Path, value: &T) -> Result<()> {
63 if let Some(parent) = path.parent() {
64 fs::create_dir_all(parent).map_err(|source| CoreError::Io {
65 path: parent.to_path_buf(),
66 source,
67 })?;
68 }
69 let raw = serde_json::to_string_pretty(value)?;
70 fs::write(path, format!("{raw}\n")).map_err(|source| CoreError::Io {
71 path: path.to_path_buf(),
72 source,
73 })?;
74 set_secure_permissions(path)?;
75 Ok(())
76}
77
78pub fn decode_secret_key(value: &str) -> Result<SigningKey> {
80 let raw = URL_SAFE_NO_PAD
81 .decode(value)
82 .map_err(|error| CoreError::Base64Decode(error.to_string()))?;
83 let bytes: [u8; 32] = raw
84 .try_into()
85 .map_err(|_| CoreError::InvalidInput("secret key must decode to 32 bytes".to_string()))?;
86 Ok(SigningKey::from_bytes(&bytes))
87}
88
89pub fn init_identity(
91 options: &ConfigPathOptions,
92 registry_url_override: Option<String>,
93) -> Result<LocalIdentity> {
94 let mut config = resolve_config(options)?;
95 if let Some(override_url) = registry_url_override {
96 let trimmed = override_url.trim().to_string();
97 if trimmed.is_empty() {
98 return Err(CoreError::InvalidInput(
99 "registryUrl cannot be empty".to_string(),
100 ));
101 }
102 config.registry_url = trimmed;
103 }
104
105 let state_options = options.with_registry_hint(config.registry_url.clone());
106 let path = identity_path(&state_options)?;
107 if path.exists() {
108 return Err(CoreError::IdentityAlreadyExists(path));
109 }
110
111 let mut secret_bytes = [0_u8; 32];
112 getrandom_fill(&mut secret_bytes)
113 .map_err(|error| CoreError::InvalidInput(error.to_string()))?;
114 let signing_key = SigningKey::from_bytes(&secret_bytes);
115 let verifying_key: VerifyingKey = signing_key.verifying_key();
116
117 let did_authority = did_authority_from_url(&config.registry_url, "registryUrl")?;
118 let did = new_human_did(&did_authority)?;
119 let identity = LocalIdentity {
120 did,
121 public_key: URL_SAFE_NO_PAD.encode(verifying_key.as_bytes()),
122 secret_key: URL_SAFE_NO_PAD.encode(signing_key.to_bytes()),
123 registry_url: config.registry_url.clone(),
124 };
125
126 write_secure_json(&path, &identity)?;
127 let _ = write_config(
128 &CliConfig {
129 registry_url: config.registry_url,
130 proxy_url: config.proxy_url,
131 api_key: config.api_key,
132 human_name: config.human_name,
133 },
134 options,
135 )?;
136 Ok(identity)
137}
138
139pub fn read_identity(options: &ConfigPathOptions) -> Result<LocalIdentity> {
141 let path = identity_path(options)?;
142 let raw = fs::read_to_string(&path).map_err(|source| {
143 if source.kind() == std::io::ErrorKind::NotFound {
144 return CoreError::IdentityNotFound(path.clone());
145 }
146 CoreError::Io {
147 path: path.clone(),
148 source,
149 }
150 })?;
151
152 serde_json::from_str::<LocalIdentity>(&raw)
153 .map_err(|source| CoreError::JsonParse { path, source })
154}
155
156#[cfg(test)]
157mod tests {
158 use tempfile::TempDir;
159
160 use crate::config::ConfigPathOptions;
161
162 use super::{decode_secret_key, init_identity, read_identity};
163
164 fn options(home: &std::path::Path) -> ConfigPathOptions {
165 ConfigPathOptions {
166 home_dir: Some(home.to_path_buf()),
167 registry_url_hint: Some("https://registry.clawdentity.com".to_string()),
168 }
169 }
170
171 #[test]
172 fn init_identity_creates_identity_and_can_read_it() {
173 let tmp = TempDir::new().expect("temp dir");
174 let opts = options(tmp.path());
175
176 let created = init_identity(&opts, None).expect("identity should initialize");
177 let loaded = read_identity(&opts).expect("identity should load");
178 assert_eq!(created.did, loaded.did);
179 assert_eq!(created.public_key, loaded.public_key);
180 }
181
182 #[test]
183 fn decode_secret_key_accepts_generated_material() {
184 let tmp = TempDir::new().expect("temp dir");
185 let opts = options(tmp.path());
186 let created = init_identity(&opts, None).expect("identity should initialize");
187 let key = decode_secret_key(&created.secret_key).expect("secret key should decode");
188 assert_eq!(key.to_bytes().len(), 32);
189 }
190}