use geth_types::{AgentId, DeviceId, KeyId, NodeId, UnixMillis, UserId}; use serde::{Deserialize, Serialize}; pub const KEYCHAIN_SIGNATURE_NAMESPACE: &str = "geth.keychain.v1@geth.local"; pub type SignedKeychainOp = geth_codec::SignedEnvelope; pub fn keychain_signing_payload(op: &KeychainOp) -> Result, geth_codec::CodecError> { geth_codec::signing_payload(KEYCHAIN_SIGNATURE_NAMESPACE, op) } pub fn keychain_signing_payload_hash( op: &KeychainOp, ) -> Result { geth_codec::signing_payload_hash(KEYCHAIN_SIGNATURE_NAMESPACE, op) } #[must_use] pub fn signed_keychain_op(op: KeychainOp, signer: KeyId, signature: Vec) -> SignedKeychainOp { geth_codec::SignedEnvelope::new(KEYCHAIN_SIGNATURE_NAMESPACE, op, signer, signature) } #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct KeychainOp { pub id: geth_types::AuthOpId, pub created_at: UnixMillis, pub kind: KeychainOpKind, } #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] #[serde(tag = "kind", rename_all = "kebab-case")] pub enum KeychainOpKind { KeychainInit, AdminKeyAdd { key: KeyId, }, AdminKeyRevoke { key: KeyId, }, UserAdd { user: UserId, name: String, }, UserRename { user: UserId, name: String, }, UserRevoke { user: UserId, }, DeviceAdd { device: DeviceId, user: UserId, }, DeviceRevoke { device: DeviceId, }, DeviceKeyAdd { device: DeviceId, key: KeyId, }, DeviceKeyRevoke { device: DeviceId, key: KeyId, }, NodeAdd { node: NodeId, device: DeviceId, name: String, }, NodeRename { node: NodeId, name: String, }, NodeRevoke { node: NodeId, }, NodeEndpointAdd { node: NodeId, endpoint: String, }, NodeEndpointRevoke { node: NodeId, endpoint: String, }, AgentBind { agent: AgentId, node: NodeId, }, } #[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] pub struct KeychainView { pub initialized: bool, pub admin_keys: Vec, pub users: Vec, pub devices: Vec, pub nodes: Vec, } pub fn reduce_keychain_ops(ops: &[KeychainOp]) -> KeychainView { let mut view = KeychainView::default(); for op in ops { match &op.kind { KeychainOpKind::KeychainInit => view.initialized = true, KeychainOpKind::AdminKeyAdd { key } if !view.admin_keys.contains(key) => { view.admin_keys.push(key.clone()); } KeychainOpKind::AdminKeyRevoke { key } => view.admin_keys.retain(|item| item != key), KeychainOpKind::UserAdd { user, .. } if !view.users.contains(user) => { view.users.push(user.clone()); } KeychainOpKind::UserRevoke { user } => view.users.retain(|item| item != user), KeychainOpKind::DeviceAdd { device, .. } if !view.devices.contains(device) => { view.devices.push(device.clone()); } KeychainOpKind::DeviceRevoke { device } => view.devices.retain(|item| item != device), KeychainOpKind::NodeAdd { node, .. } if !view.nodes.contains(node) => { view.nodes.push(node.clone()); } KeychainOpKind::NodeRevoke { node } => view.nodes.retain(|item| item != node), _ => {} } } view } #[cfg(test)] mod tests { use super::*; #[test] fn keychain_structs_roundtrip() { let op = KeychainOp { id: "op:1".into(), created_at: UnixMillis(1), kind: KeychainOpKind::UserAdd { user: "user:eric".into(), name: "Eric".to_owned(), }, }; let json = serde_json::to_string(&op).expect("json"); let decoded: KeychainOp = serde_json::from_str(&json).expect("decode"); assert_eq!(decoded, op); } #[test] fn keychain_signing_payload_is_canonical_and_namespaced() { let op = KeychainOp { id: "op:1".into(), created_at: UnixMillis(1), kind: KeychainOpKind::AdminKeyAdd { key: "key:admin".into(), }, }; assert_eq!( keychain_signing_payload(&op).expect("payload"), keychain_signing_payload(&op).expect("payload again") ); assert_ne!( keychain_signing_payload_hash(&op).expect("hash"), geth_codec::hash_canonical(&op).expect("raw op hash") ); let signed = signed_keychain_op(op.clone(), "key:admin".into(), vec![1, 2, 3]); assert_eq!(signed.namespace(), KEYCHAIN_SIGNATURE_NAMESPACE); assert_eq!(signed.payload(), &op); } }