pub mod json_rpc_client;
use json_rpc_client::client::JSONRPCClient;
use json_rpc_client::protocol::{
build_rpcjson_message, parse_rpcjson_response, JsonProtocolResponse,
};
use serde::{Deserialize, Serialize};
use std::str::FromStr;
const BUNKR_JSON_PROTOCOL_VERSION: &str = "1.0";
const BUNKR_RPC_METHOD: &str = "CommandProxy.HandleCommand";
pub enum Command {
NewTextSecret,
NewSSHKey,
NewFileSecret,
NewGroup,
ImportSSHKey,
ListSecrets,
ListDevices,
ListGroups,
SendDevice,
ReceiveDevice,
RemoveDevice,
RemoveLocal,
Rename,
Create,
Write,
Access,
Grant,
Revoke,
Delete,
ReceiveCapability,
ResetTriples,
NoOp,
SecretInfo,
SignECDSA,
SSHPublicData,
SignIn,
ConfirmSignin,
}
impl Command {
pub fn to_string(&self) -> String {
let res = match self {
Command::NewTextSecret => "new-text-secret",
Command::NewSSHKey => "new-ssh-key",
Command::NewFileSecret => "new-file-secret",
Command::NewGroup => "new-group",
Command::ImportSSHKey => "import-ssh-key",
Command::ListSecrets => "list-secrets",
Command::ListDevices => "list-devices",
Command::ListGroups => "list-groups",
Command::SendDevice => "send-device",
Command::ReceiveDevice => "receive-device",
Command::RemoveDevice => "remove-device",
Command::RemoveLocal => "remove-local",
Command::Rename => "rename",
Command::Create => "create",
Command::Write => "write",
Command::Access => "access",
Command::Grant => "grant",
Command::Revoke => "revoke",
Command::Delete => "delete",
Command::ReceiveCapability => "receive-capability",
Command::ResetTriples => "reset-triples",
Command::NoOp => "noop-test",
Command::SecretInfo => "secret-info",
Command::SignECDSA => "sign-ecdsa",
Command::SSHPublicData => "ssh-public-data",
Command::SignIn => "sigin",
Command::ConfirmSignin => "confirm-signin",
};
res.to_string()
}
}
impl FromStr for Command {
type Err = &'static str;
fn from_str(command: &str) -> Result<Self, Self::Err> {
match command {
"new-text-secret" => Ok(Command::NewTextSecret),
"new-ssh-key" => Ok(Command::NewSSHKey),
"new-file-secret" => Ok(Command::NewFileSecret),
"new-group" => Ok(Command::NewGroup),
"import-ssh-key" => Ok(Command::ImportSSHKey),
"list-secrets" => Ok(Command::ListSecrets),
"list-devices" => Ok(Command::ListDevices),
"list-groups" => Ok(Command::ListGroups),
"send-device" => Ok(Command::SendDevice),
"receive-device" => Ok(Command::ReceiveDevice),
"remove-device" => Ok(Command::RemoveDevice),
"remove-local" => Ok(Command::RemoveLocal),
"rename" => Ok(Command::Rename),
"create" => Ok(Command::Create),
"write" => Ok(Command::Write),
"access" => Ok(Command::Access),
"grant" => Ok(Command::Grant),
"revoke" => Ok(Command::Revoke),
"delete" => Ok(Command::Delete),
"receive-capability" => Ok(Command::ReceiveCapability),
"reset-triples" => Ok(Command::ResetTriples),
"noop-test" => Ok(Command::NoOp),
"secret-info" => Ok(Command::SecretInfo),
"sign-ecdsa" => Ok(Command::SignECDSA),
"ssh-public-data" => Ok(Command::SSHPublicData),
"sigin" => Ok(Command::SignIn),
"confirm-signin" => Ok(Command::ConfirmSignin),
_ => Err("Command not supported"),
}
}
}
pub enum SecretType {
ECDSASECP256k1Key,
ECDSAP256Key,
HMACKey,
GenericGF256,
GenericPF,
}
impl SecretType {
pub fn to_string(&self) -> String {
let res = match self {
SecretType::ECDSASECP256k1Key => "ECDSA-SECP256k1",
SecretType::ECDSAP256Key => "ECDSA-P256",
SecretType::HMACKey => "HMAC",
SecretType::GenericGF256 => "GENERIC-GF256",
SecretType::GenericPF => "GENERIC-PF",
};
res.to_string()
}
}
impl FromStr for SecretType {
type Err = &'static str;
fn from_str(secret_type: &str) -> Result<Self, Self::Err> {
match secret_type {
"ECDSA-SECP256k1" => Ok(SecretType::ECDSASECP256k1Key),
"ECDSA-P256" => Ok(SecretType::ECDSAP256Key),
"HMAC" => Ok(SecretType::HMACKey),
"GENERIC-GF256" => Ok(SecretType::GenericGF256),
"GENERIC-PF" => Ok(SecretType::GenericPF),
_ => Err("Secret type not supported"),
}
}
}
pub enum ContentType {
B64,
Text,
}
impl ContentType {
pub fn to_string(&self) -> String {
let res = match self {
ContentType::B64 => "b64",
ContentType::Text => "text",
};
res.to_string()
}
}
impl FromStr for ContentType {
type Err = &'static str;
fn from_str(content_type: &str) -> Result<Self, Self::Err> {
match content_type.to_lowercase().as_str() {
"b64" => Ok(ContentType::B64),
"text" => Ok(ContentType::Text),
_ => Err("Content type not supported"),
}
}
}
pub enum AccessMode {
B64,
Text,
File,
}
impl AccessMode {
pub fn to_string(&self) -> String {
let res = match self {
AccessMode::B64 => "b64",
AccessMode::Text => "text",
AccessMode::File => "file",
};
res.to_string()
}
}
impl FromStr for AccessMode {
type Err = &'static str;
fn from_str(mode: &str) -> Result<Self, Self::Err> {
match mode.to_lowercase().as_str() {
"b64" => Ok(AccessMode::B64),
"text" => Ok(AccessMode::Text),
"file" => Ok(AccessMode::File),
_ => Err("Content type not supported"),
}
}
}
type Response = serde_json::Value;
#[derive(Serialize, Deserialize, Debug)]
struct BunkrResult {
#[serde(rename = "Result")]
result: Response,
#[serde(rename = "Error")]
error: String,
}
#[derive(Serialize, Deserialize, Debug)]
struct OperationArgs {
#[serde(rename = "Command")]
command: String,
#[serde(rename = "Args")]
args: Vec<String>,
}
pub struct Runkr {
client: JSONRPCClient,
}
impl Runkr {
pub fn new(address: &str) -> Runkr {
Runkr {
client: JSONRPCClient::new(address),
}
}
fn handle_response(&mut self, response: String) -> Result<Response, String> {
match parse_rpcjson_response::<JsonProtocolResponse<BunkrResult>>(response.as_str()) {
Ok(bunkr_result) => match bunkr_result.error {
Some(err) => return Err(err),
None => {
let res = bunkr_result.result.result;
return Ok(res);
}
},
Err(e) => Err(format!("Error parsing response: {}", e)),
}
}
fn exec_command(
&mut self,
command_name: Command,
args: Vec<String>,
) -> Result<Response, String> {
let message = build_rpcjson_message(
BUNKR_JSON_PROTOCOL_VERSION.to_string(),
BUNKR_RPC_METHOD.to_string(),
vec![OperationArgs {
command: command_name.to_string(),
args,
}],
);
if !self.client.is_connected() {
self.client.connect()?;
}
match self.client.send(message) {
Ok(response) => {
match self.client.disconnect() {
_ => {}
}
return self.handle_response(response);
}
Err(err) => {
match self.client.disconnect() {
_ => {}
}
return Err(err);
}
};
}
pub fn new_text_secret(
&mut self,
secret_name: &str,
content: &str,
) -> Result<Response, String> {
self.exec_command(
Command::NewTextSecret,
vec![secret_name.to_string(), content.to_string()],
)
}
pub fn new_ssh_key(&mut self, secret_name: &str) -> Result<Response, String> {
self.exec_command(Command::NewSSHKey, vec![secret_name.to_string()])
}
pub fn new_file_secret(
&mut self,
secret_name: &str,
file_path: &str,
) -> Result<Response, String> {
self.exec_command(
Command::NewFileSecret,
vec![secret_name.to_string(), file_path.to_string()],
)
}
pub fn new_group(&mut self, group_name: &str) -> Result<Response, String> {
self.exec_command(Command::NewGroup, vec![group_name.to_string()])
}
pub fn import_ssh_key(
&mut self,
secret_name: &str,
file_path: &str,
) -> Result<Response, String> {
self.exec_command(
Command::ImportSSHKey,
vec![secret_name.to_string(), file_path.to_string()],
)
}
pub fn list_secrets(&mut self) -> Result<Response, String> {
self.exec_command(Command::ListSecrets, vec![])
}
pub fn list_devices(&mut self) -> Result<Response, String> {
self.exec_command(Command::ListDevices, vec![])
}
pub fn list_groups(&mut self) -> Result<Response, String> {
self.exec_command(Command::ListGroups, vec![])
}
pub fn send_device(&mut self, device_name: Option<&str>) -> Result<Response, String> {
match device_name {
Some(name) => self.exec_command(Command::SendDevice, vec![name.to_string()]),
None => self.exec_command(Command::SendDevice, vec![]),
}
}
pub fn receive_device(&mut self, url: &str) -> Result<Response, String> {
self.exec_command(Command::ReceiveDevice, vec![url.to_string()])
}
pub fn remove_device(&mut self, device_name: &str) -> Result<Response, String> {
self.exec_command(Command::RemoveDevice, vec![device_name.to_string()])
}
pub fn remove_local(&mut self, secret_name: &str) -> Result<Response, String> {
self.exec_command(Command::RemoveLocal, vec![secret_name.to_string()])
}
pub fn rename(&mut self, old_name: &str, new_name: &str) -> Result<Response, String> {
self.exec_command(
Command::Rename,
vec![old_name.to_string(), new_name.to_string()],
)
}
pub fn create(
&mut self,
secret_name: &str,
secret_type: SecretType,
) -> Result<Response, String> {
self.exec_command(
Command::Create,
vec![secret_name.to_string(), secret_type.to_string()],
)
}
pub fn write(
&mut self,
secret_name: &str,
content: &str,
content_type: Option<ContentType>,
) -> Result<Response, String> {
match content_type {
Some(t) => self.exec_command(
Command::Write,
vec![secret_name.to_string(), t.to_string(), content.to_string()],
),
None => self.exec_command(
Command::Write,
vec![
secret_name.to_string(),
ContentType::B64.to_string(),
content.to_string(),
],
),
}
}
pub fn access(
&mut self,
secret_name: &str,
mode: AccessMode,
fpath: Option<&str>,
) -> Result<Response, String> {
match mode {
AccessMode::File => match fpath {
Some(path) => self.exec_command(
Command::Access,
vec![
secret_name.to_string(),
AccessMode::File.to_string(),
path.to_string(),
],
),
None => Err("A file path must be specified for File mode".to_string()),
},
_ => self.exec_command(
Command::Access,
vec![secret_name.to_string(), mode.to_string()],
),
}
}
pub fn grant(
&mut self,
target: &str,
secret_name: &str,
admin: bool,
) -> Result<Response, String> {
match admin {
true => self.exec_command(
Command::Grant,
vec![
target.to_string(),
secret_name.to_string(),
"admin".to_string(),
],
),
false => self.exec_command(
Command::Grant,
vec![target.to_string(), secret_name.to_string()],
),
}
}
pub fn revoke(&mut self, target: &str, secret_name: &str) -> Result<Response, String> {
self.exec_command(
Command::Revoke,
vec![target.to_string(), secret_name.to_string()],
)
}
pub fn delete(&mut self, secret_name: &str) -> Result<Response, String> {
self.exec_command(Command::Delete, vec![secret_name.to_string()])
}
pub fn receive_capability(&mut self, url: &str) -> Result<Response, String> {
self.exec_command(Command::ReceiveCapability, vec![url.to_string()])
}
pub fn reset_triples(&mut self, secret_name: &str) -> Result<Response, String> {
self.exec_command(Command::ResetTriples, vec![secret_name.to_string()])
}
pub fn noop(&mut self, secret_name: &str) -> Result<Response, String> {
self.exec_command(Command::NoOp, vec![secret_name.to_string()])
}
pub fn secret_info(&mut self, secret_name: &str) -> Result<Response, String> {
self.exec_command(Command::SecretInfo, vec![secret_name.to_string()])
}
pub fn sign_ecdsa(
&mut self,
secret_name: &str,
hash_content: &str,
) -> Result<Response, String> {
self.exec_command(
Command::SignECDSA,
vec![secret_name.to_string(), hash_content.to_string()],
)
}
pub fn ssh_public_data(&mut self, secret_name: &str) -> Result<Response, String> {
self.exec_command(Command::SSHPublicData, vec![secret_name.to_string()])
}
pub fn signin(&mut self, email: &str, device_name: &str) -> Result<Response, String> {
self.exec_command(
Command::SignIn,
vec![email.to_string(), device_name.to_string()],
)
}
pub fn confirm_signin(&mut self, email: &str, code: &str) -> Result<Response, String> {
self.exec_command(
Command::ConfirmSignin,
vec![email.to_string(), code.to_string()],
)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::json_rpc_client::protocol::JsonProtocolMessage;
use serde_json::Value;
use std::fs::remove_file;
use std::io::prelude::*;
use std::os::unix::net::UnixListener;
use std::thread;
use std::time;
const TMP_SOCK: &str = "/tmp/punkr.sock";
fn mock_server(response: String) {
let listener = UnixListener::bind(TMP_SOCK).unwrap();
match listener.accept() {
Ok((mut stream, _)) => loop {
let mut buff = [0 as u8; 1024];
let mut handle = stream.try_clone().unwrap().take(1024);
handle.read(&mut buff).unwrap();
let message = String::from_utf8(buff.to_vec())
.unwrap()
.trim_end_matches(char::from(0))
.to_string();
println!("Received message: {}", message);
let message: JsonProtocolMessage<OperationArgs> =
serde_json::from_str(message.as_str()).unwrap();
if &message.params[0].args[0] == "end" {
break;
}
stream.write_all(response.as_bytes()).unwrap();
},
Err(e) => println!("accept function failed: {:?}", e),
}
}
#[test]
fn test_punkr_client() {
let response_message: JsonProtocolResponse<BunkrResult> = JsonProtocolResponse {
id: uuid::Uuid::new_v4().to_string(),
result: BunkrResult {
result: Value::String("Foo".to_string()),
error: "".to_string(),
},
error: None,
};
let response = serde_json::to_string(&response_message).unwrap();
let t = thread::spawn(move || mock_server(response));
let milis = time::Duration::from_millis(500);
thread::sleep(milis);
let mut runkr = Runkr::new(TMP_SOCK);
let res = runkr
.exec_command(
Command::Create,
vec!["foo".to_string(), "foo_content".to_string()],
)
.unwrap();
assert_eq!(res, "Foo".to_string());
runkr
.exec_command(Command::NoOp, vec!["end".to_string()])
.unwrap_or_default();
t.join().unwrap_or_default();
remove_file(TMP_SOCK).unwrap();
}
}