Compare commits
5 Commits
v0.5.0
...
27c2f467c4
Author | SHA1 | Date | |
---|---|---|---|
27c2f467c4 | |||
cab5ec40cc | |||
5cf848f7fe | |||
a32e36be7e | |||
10231df860 |
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "creddy",
|
||||
"version": "0.5.0",
|
||||
"version": "0.5.3",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
|
67
src-tauri/Cargo.lock
generated
67
src-tauri/Cargo.lock
generated
@ -1071,7 +1071,7 @@ dependencies = [
|
||||
"cocoa-foundation",
|
||||
"core-foundation",
|
||||
"core-graphics",
|
||||
"foreign-types",
|
||||
"foreign-types 0.5.0",
|
||||
"libc",
|
||||
"objc",
|
||||
]
|
||||
@ -1146,7 +1146,7 @@ dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"core-foundation",
|
||||
"core-graphics-types",
|
||||
"foreign-types",
|
||||
"foreign-types 0.5.0",
|
||||
"libc",
|
||||
]
|
||||
|
||||
@ -1196,7 +1196,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "creddy"
|
||||
version = "0.4.9"
|
||||
version = "0.5.3"
|
||||
dependencies = [
|
||||
"argon2",
|
||||
"auto-launch",
|
||||
@ -1211,13 +1211,17 @@ dependencies = [
|
||||
"futures",
|
||||
"is-terminal",
|
||||
"once_cell",
|
||||
"openssl",
|
||||
"rfd 0.13.0",
|
||||
"rsa",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha2",
|
||||
"signature 2.2.0",
|
||||
"sodiumoxide",
|
||||
"sqlx",
|
||||
"ssh-agent-lib",
|
||||
"ssh-encoding",
|
||||
"ssh-key",
|
||||
"strum",
|
||||
"strum_macros",
|
||||
@ -1841,6 +1845,15 @@ version = "1.0.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
|
||||
|
||||
[[package]]
|
||||
name = "foreign-types"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
|
||||
dependencies = [
|
||||
"foreign-types-shared 0.1.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "foreign-types"
|
||||
version = "0.5.0"
|
||||
@ -1848,7 +1861,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965"
|
||||
dependencies = [
|
||||
"foreign-types-macros",
|
||||
"foreign-types-shared",
|
||||
"foreign-types-shared 0.3.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1862,6 +1875,12 @@ dependencies = [
|
||||
"syn 2.0.68",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "foreign-types-shared"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
|
||||
|
||||
[[package]]
|
||||
name = "foreign-types-shared"
|
||||
version = "0.3.1"
|
||||
@ -3426,12 +3445,50 @@ version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381"
|
||||
|
||||
[[package]]
|
||||
name = "openssl"
|
||||
version = "0.10.64"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "95a0481286a310808298130d22dd1fef0fa571e05a8f44ec801801e84b216b1f"
|
||||
dependencies = [
|
||||
"bitflags 2.6.0",
|
||||
"cfg-if",
|
||||
"foreign-types 0.3.2",
|
||||
"libc",
|
||||
"once_cell",
|
||||
"openssl-macros",
|
||||
"openssl-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "openssl-macros"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.68",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "openssl-probe"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
|
||||
|
||||
[[package]]
|
||||
name = "openssl-sys"
|
||||
version = "0.9.102"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c597637d56fbc83893a35eb0dd04b2b8e7a50c91e64e9493e398b5df4fb45fa2"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
"pkg-config",
|
||||
"vcpkg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "option-ext"
|
||||
version = "0.2.0"
|
||||
@ -4788,7 +4845,7 @@ dependencies = [
|
||||
"bytemuck",
|
||||
"cfg_aliases",
|
||||
"core-graphics",
|
||||
"foreign-types",
|
||||
"foreign-types 0.5.0",
|
||||
"js-sys",
|
||||
"log",
|
||||
"objc2",
|
||||
|
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "creddy"
|
||||
version = "0.5.0"
|
||||
version = "0.5.3"
|
||||
description = "A friendly AWS credentials manager"
|
||||
authors = ["Joseph Montanaro"]
|
||||
license = ""
|
||||
@ -58,6 +58,10 @@ tokio-stream = "0.1.15"
|
||||
sqlx = { version = "0.7.4", features = ["sqlite", "runtime-tokio", "uuid"] }
|
||||
tokio-util = { version = "0.7.11", features = ["codec"] }
|
||||
futures = "0.3.30"
|
||||
openssl = "0.10.64"
|
||||
rsa = "0.9.6"
|
||||
sha2 = "0.10.8"
|
||||
ssh-encoding = "0.2.0"
|
||||
|
||||
[features]
|
||||
# by default Tauri runs in production mode
|
||||
|
@ -53,6 +53,7 @@ pub fn run() -> tauri::Result<()> {
|
||||
ipc::delete_credential,
|
||||
ipc::list_credentials,
|
||||
ipc::sshkey_from_file,
|
||||
ipc::sshkey_from_private_key,
|
||||
ipc::get_config,
|
||||
ipc::save_config,
|
||||
ipc::launch_terminal,
|
||||
|
@ -11,17 +11,12 @@ use std::{
|
||||
|
||||
|
||||
fn main() {
|
||||
let args = cli::parser().get_matches();
|
||||
if let Some(true) = args.get_one::<bool>("help") {
|
||||
cli::parser().print_help().unwrap(); // if we can't print help we can't print an error
|
||||
process::exit(0);
|
||||
}
|
||||
|
||||
let res = match args.subcommand() {
|
||||
let global_matches = cli::parser().get_matches();
|
||||
let res = match global_matches.subcommand() {
|
||||
None | Some(("run", _)) => launch_gui(),
|
||||
Some(("get", m)) => cli::get(m),
|
||||
Some(("exec", m)) => cli::exec(m),
|
||||
Some(("shortcut", m)) => cli::invoke_shortcut(m),
|
||||
Some(("get", m)) => cli::get(m, &global_matches),
|
||||
Some(("exec", m)) => cli::exec(m, &global_matches),
|
||||
Some(("shortcut", m)) => cli::invoke_shortcut(m, &global_matches),
|
||||
_ => unreachable!("Unknown subcommand"),
|
||||
};
|
||||
|
||||
@ -35,7 +30,7 @@ fn main() {
|
||||
fn launch_gui() -> Result<(), CliError> {
|
||||
let mut path = env::current_exe()?;
|
||||
path.pop(); // bin dir
|
||||
|
||||
|
||||
// binaries are colocated in dev, but not in production
|
||||
#[cfg(not(debug_assertions))]
|
||||
path.pop(); // install dir
|
||||
|
@ -1,4 +1,5 @@
|
||||
use std::ffi::OsString;
|
||||
use std::path::PathBuf;
|
||||
use std::process::Command as ChildCommand;
|
||||
#[cfg(windows)]
|
||||
use std::time::Duration;
|
||||
@ -9,6 +10,7 @@ use clap::{
|
||||
ArgMatches,
|
||||
ArgAction,
|
||||
builder::PossibleValuesParser,
|
||||
value_parser,
|
||||
};
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
|
||||
@ -37,6 +39,14 @@ pub fn parser() -> Command<'static> {
|
||||
Command::new("creddy")
|
||||
.version(env!("CARGO_PKG_VERSION"))
|
||||
.about("A friendly AWS credentials manager")
|
||||
.arg(
|
||||
Arg::new("server_addr")
|
||||
.short('a')
|
||||
.long("server-addr")
|
||||
.takes_value(true)
|
||||
.value_parser(value_parser!(PathBuf))
|
||||
.help("Connect to the main Creddy process at this address")
|
||||
)
|
||||
.subcommand(
|
||||
Command::new("run")
|
||||
.about("Launch Creddy")
|
||||
@ -71,6 +81,7 @@ pub fn parser() -> Command<'static> {
|
||||
Arg::new("name")
|
||||
.short('n')
|
||||
.long("name")
|
||||
.takes_value(true)
|
||||
.help("If unspecified, use default credentials")
|
||||
)
|
||||
.arg(
|
||||
@ -91,11 +102,12 @@ pub fn parser() -> Command<'static> {
|
||||
}
|
||||
|
||||
|
||||
pub fn get(args: &ArgMatches) -> Result<(), CliError> {
|
||||
pub fn get(args: &ArgMatches, global_args: &ArgMatches) -> Result<(), CliError> {
|
||||
let name = args.get_one("name").cloned();
|
||||
let base = *args.get_one("base").unwrap_or(&false);
|
||||
|
||||
let output = match make_request(&Request::GetAwsCredentials { name, base })? {
|
||||
let addr = global_args.get_one("server_addr").cloned();
|
||||
|
||||
let output = match make_request(addr, &Request::GetAwsCredentials { name, base })? {
|
||||
Response::AwsBase(creds) => serde_json::to_string(&creds).unwrap(),
|
||||
Response::AwsSession(creds) => serde_json::to_string(&creds).unwrap(),
|
||||
r => return Err(RequestError::Unexpected(r).into()),
|
||||
@ -105,17 +117,18 @@ pub fn get(args: &ArgMatches) -> Result<(), CliError> {
|
||||
}
|
||||
|
||||
|
||||
pub fn exec(args: &ArgMatches) -> Result<(), CliError> {
|
||||
pub fn exec(args: &ArgMatches, global_args: &ArgMatches) -> Result<(), CliError> {
|
||||
let name = args.get_one("name").cloned();
|
||||
let base = *args.get_one("base").unwrap_or(&false);
|
||||
let addr = global_args.get_one("server_addr").cloned();
|
||||
let mut cmd_line = args.get_many("command")
|
||||
.ok_or(ExecError::NoCommand)?;
|
||||
|
||||
let cmd_name: &String = cmd_line.next().unwrap(); // Clap guarantees that there will be at least one
|
||||
let mut cmd = ChildCommand::new(cmd_name);
|
||||
cmd.args(cmd_line);
|
||||
|
||||
match make_request(&Request::GetAwsCredentials { name, base })? {
|
||||
|
||||
match make_request(addr, &Request::GetAwsCredentials { name, base })? {
|
||||
Response::AwsBase(creds) => {
|
||||
cmd.env("AWS_ACCESS_KEY_ID", creds.access_key_id);
|
||||
cmd.env("AWS_SECRET_ACCESS_KEY", creds.secret_access_key);
|
||||
@ -159,7 +172,8 @@ pub fn exec(args: &ArgMatches) -> Result<(), CliError> {
|
||||
}
|
||||
|
||||
|
||||
pub fn invoke_shortcut(args: &ArgMatches) -> Result<(), CliError> {
|
||||
pub fn invoke_shortcut(args: &ArgMatches, global_args: &ArgMatches) -> Result<(), CliError> {
|
||||
let addr = global_args.get_one("server_addr").cloned();
|
||||
let action = match args.get_one::<String>("action").map(|s| s.as_str()) {
|
||||
Some("show_window") => ShortcutAction::ShowWindow,
|
||||
Some("launch_terminal") => ShortcutAction::LaunchTerminal,
|
||||
@ -167,7 +181,7 @@ pub fn invoke_shortcut(args: &ArgMatches) -> Result<(), CliError> {
|
||||
};
|
||||
|
||||
let req = Request::InvokeShortcut(action);
|
||||
match make_request(&req) {
|
||||
match make_request(addr, &req) {
|
||||
Ok(Response::Empty) => Ok(()),
|
||||
Ok(r) => Err(RequestError::Unexpected(r).into()),
|
||||
Err(e) => Err(e.into()),
|
||||
@ -176,12 +190,12 @@ pub fn invoke_shortcut(args: &ArgMatches) -> Result<(), CliError> {
|
||||
|
||||
|
||||
#[tokio::main]
|
||||
async fn make_request(req: &Request) -> Result<Response, RequestError> {
|
||||
async fn make_request(addr: Option<PathBuf>, req: &Request) -> Result<Response, RequestError> {
|
||||
let mut data = serde_json::to_string(req).unwrap();
|
||||
// server expects newline marking end of request
|
||||
data.push('\n');
|
||||
|
||||
let mut stream = connect().await?;
|
||||
let mut stream = connect(addr).await?;
|
||||
stream.write_all(&data.as_bytes()).await?;
|
||||
|
||||
let mut buf = Vec::with_capacity(1024);
|
||||
@ -192,10 +206,10 @@ async fn make_request(req: &Request) -> Result<Response, RequestError> {
|
||||
|
||||
|
||||
#[cfg(windows)]
|
||||
async fn connect() -> Result<NamedPipeClient, std::io::Error> {
|
||||
async fn connect(addr: Option<PathBuf>) -> Result<NamedPipeClient, std::io::Error> {
|
||||
// apparently attempting to connect can fail if there's already a client connected
|
||||
loop {
|
||||
let addr = srv::addr("creddy-server");
|
||||
let addr = addr.unwrap_or_else(|| srv::addr("creddy-server"));
|
||||
match ClientOptions::new().open(&addr) {
|
||||
Ok(stream) => return Ok(stream),
|
||||
Err(e) if e.raw_os_error() == Some(ERROR_PIPE_BUSY.0 as i32) => (),
|
||||
@ -207,7 +221,7 @@ async fn connect() -> Result<NamedPipeClient, std::io::Error> {
|
||||
|
||||
|
||||
#[cfg(unix)]
|
||||
async fn connect() -> Result<UnixStream, std::io::Error> {
|
||||
let path = srv::addr("creddy-server");
|
||||
async fn connect(addr: Option<PathBuf>) -> Result<UnixStream, std::io::Error> {
|
||||
let path = addr.unwrap_or_else(|| srv::addr("creddy-server"));
|
||||
UnixStream::connect(&path).await
|
||||
}
|
||||
|
@ -76,7 +76,7 @@ impl PersistentCredential for AwsBaseCredential {
|
||||
access_key_id,
|
||||
secret_key_enc,
|
||||
nonce
|
||||
)
|
||||
)
|
||||
VALUES (?, ?, ?, ?);",
|
||||
id, self.access_key_id, ciphertext, nonce_bytes,
|
||||
).execute(&mut **txn).await?;
|
||||
@ -203,19 +203,6 @@ mod tests {
|
||||
)
|
||||
}
|
||||
|
||||
fn test_uuid() -> Uuid {
|
||||
Uuid::try_parse("00000000-0000-0000-0000-000000000000").unwrap()
|
||||
}
|
||||
|
||||
fn test_uuid_2() -> Uuid {
|
||||
Uuid::try_parse("ffffffff-ffff-ffff-ffff-ffffffffffff").unwrap()
|
||||
}
|
||||
|
||||
fn test_uuid_random() -> Uuid {
|
||||
let bytes = Crypto::salt();
|
||||
Uuid::from_slice(&bytes[..16]).unwrap()
|
||||
}
|
||||
|
||||
|
||||
#[sqlx::test(fixtures("aws_credentials"))]
|
||||
async fn test_load(pool: SqlitePool) {
|
||||
@ -254,5 +241,5 @@ mod tests {
|
||||
|
||||
assert_eq!(&creds().into_credential(), &list[0]);
|
||||
assert_eq!(&creds_2().into_credential(), &list[1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -112,15 +112,16 @@ impl CredentialRecord {
|
||||
Ok(Self::from_parts(row, credential))
|
||||
}
|
||||
|
||||
// pub async fn load(id: &Uuid, crypto: &Crypto, pool: &SqlitePool) -> Result<Self, LoadCredentialsError> {
|
||||
// let row: CredentialRow = sqlx::query_as("SELECT * FROM credentials WHERE id = ?")
|
||||
// .bind(id)
|
||||
// .fetch_optional(pool)
|
||||
// .await?
|
||||
// .ok_or(LoadCredentialsError::NoCredentials)?;
|
||||
#[cfg(test)]
|
||||
pub async fn load(id: &Uuid, crypto: &Crypto, pool: &SqlitePool) -> Result<Self, LoadCredentialsError> {
|
||||
let row: CredentialRow = sqlx::query_as("SELECT * FROM credentials WHERE id = ?")
|
||||
.bind(id)
|
||||
.fetch_optional(pool)
|
||||
.await?
|
||||
.ok_or(LoadCredentialsError::NoCredentials)?;
|
||||
|
||||
// Self::load_credential(row, crypto, pool).await
|
||||
// }
|
||||
Self::load_credential(row, crypto, pool).await
|
||||
}
|
||||
|
||||
pub async fn load_by_name(name: &str, crypto: &Crypto, pool: &SqlitePool) -> Result<Self, LoadCredentialsError> {
|
||||
let row: CredentialRow = sqlx::query_as("SELECT * FROM credentials WHERE name = ?")
|
||||
@ -134,7 +135,7 @@ impl CredentialRecord {
|
||||
|
||||
pub async fn load_default(credential_type: &str, crypto: &Crypto, pool: &SqlitePool) -> Result<Self, LoadCredentialsError> {
|
||||
let row: CredentialRow = sqlx::query_as(
|
||||
"SELECT * FROM credentials
|
||||
"SELECT * FROM credentials
|
||||
WHERE credential_type = ? AND is_default = 1"
|
||||
).bind(credential_type)
|
||||
.fetch_optional(pool)
|
||||
@ -419,7 +420,7 @@ mod uuid_tests {
|
||||
#[test]
|
||||
fn test_serialize_deserialize_uuid() {
|
||||
let buf = Crypto::salt();
|
||||
let expected = UuidWrapper{
|
||||
let expected = UuidWrapper{
|
||||
id: Uuid::from_slice(&buf[..16]).unwrap()
|
||||
};
|
||||
let serialized = serde_json::to_string(&expected).unwrap();
|
||||
|
@ -12,6 +12,8 @@ use serde::ser::{
|
||||
SerializeStruct,
|
||||
};
|
||||
use serde::de::{self, Visitor};
|
||||
use sha2::{Sha256, Sha512};
|
||||
use signature::{Signer, SignatureEncoding};
|
||||
use sqlx::{
|
||||
FromRow,
|
||||
Sqlite,
|
||||
@ -19,11 +21,15 @@ use sqlx::{
|
||||
Transaction,
|
||||
types::Uuid,
|
||||
};
|
||||
use ssh_agent_lib::proto::message::Identity;
|
||||
use ssh_agent_lib::proto::message::{
|
||||
Identity,
|
||||
SignRequest,
|
||||
};
|
||||
use ssh_encoding::Encode;
|
||||
use ssh_key::{
|
||||
Algorithm,
|
||||
LineEnding,
|
||||
private::PrivateKey,
|
||||
private::{PrivateKey, KeypairData},
|
||||
public::PublicKey,
|
||||
};
|
||||
use tokio_stream::StreamExt;
|
||||
@ -74,11 +80,26 @@ impl SshKey {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn from_private_key(private_key: &str, passphrase: &str) -> Result<SshKey, LoadSshKeyError> {
|
||||
let mut privkey = PrivateKey::from_openssh(private_key)?;
|
||||
if privkey.is_encrypted() {
|
||||
privkey = privkey.decrypt(passphrase)
|
||||
.map_err(|_| LoadSshKeyError::InvalidPassphrase)?;
|
||||
}
|
||||
|
||||
Ok(SshKey {
|
||||
algorithm: privkey.algorithm(),
|
||||
comment: privkey.comment().into(),
|
||||
public_key: privkey.public_key().clone(),
|
||||
private_key: privkey,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn name_from_pubkey(pubkey: &[u8], pool: &SqlitePool) -> Result<String, LoadCredentialsError> {
|
||||
let row = sqlx::query!(
|
||||
"SELECT c.name
|
||||
FROM credentials c
|
||||
JOIN ssh_credentials s
|
||||
JOIN ssh_credentials s
|
||||
ON s.id = c.id
|
||||
WHERE s.public_key = ?",
|
||||
pubkey
|
||||
@ -104,6 +125,33 @@ impl SshKey {
|
||||
|
||||
Ok(identities)
|
||||
}
|
||||
|
||||
pub fn sign_request(&self, req: &SignRequest) -> Result<Vec<u8>, HandlerError> {
|
||||
let mut sig = Vec::new();
|
||||
match self.private_key.key_data() {
|
||||
KeypairData::Rsa(keypair) => {
|
||||
// 2 is the flag value for `SSH_AGENT_RSA_SHA2_256`
|
||||
if req.flags & 2 > 0 {
|
||||
let signer = rsa::pkcs1v15::SigningKey::<Sha256>::try_from(keypair)?;
|
||||
let sig_data = signer.try_sign(&req.data)?.to_vec();
|
||||
"rsa-sha-256".encode(&mut sig)?;
|
||||
sig_data.encode(&mut sig)?;
|
||||
}
|
||||
else {
|
||||
let signer = rsa::pkcs1v15::SigningKey::<Sha512>::try_from(keypair)?;
|
||||
let sig_data = signer.try_sign(&req.data)?.to_vec();
|
||||
"rsa-sha2-512".encode(&mut sig)?;
|
||||
sig_data.encode(&mut sig)?;
|
||||
}
|
||||
},
|
||||
_ => {
|
||||
let sig_data = self.private_key.try_sign(&req.data)?;
|
||||
self.algorithm.as_str().encode(&mut sig)?;
|
||||
sig_data.as_bytes().encode(&mut sig)?;
|
||||
},
|
||||
}
|
||||
Ok(sig)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -120,7 +168,7 @@ impl PersistentCredential for SshKey {
|
||||
let nonce = XNonce::clone_from_slice(&row.nonce);
|
||||
let privkey_bytes = crypto.decrypt(&nonce, &row.private_key_enc)?;
|
||||
|
||||
|
||||
|
||||
let algorithm = Algorithm::new(&row.algorithm)
|
||||
.map_err(|_| LoadCredentialsError::InvalidData)?;
|
||||
let public_key = PublicKey::from_bytes(&row.public_key)
|
||||
@ -250,7 +298,6 @@ fn deserialize_algorithm<'de, D>(deserializer: D) -> Result<Algorithm, D::Error>
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::fs::{self, File};
|
||||
use ssh_key::Fingerprint;
|
||||
use sqlx::types::uuid::uuid;
|
||||
use super::*;
|
||||
|
||||
@ -293,7 +340,7 @@ mod tests {
|
||||
let k = rsa_plain();
|
||||
assert_eq!(k.algorithm.as_str(), "ssh-rsa");
|
||||
assert_eq!(&k.comment, "hello world");
|
||||
|
||||
|
||||
assert_eq!(
|
||||
k.public_key.fingerprint(Default::default()),
|
||||
k.private_key.fingerprint(Default::default()),
|
||||
@ -311,7 +358,7 @@ mod tests {
|
||||
let k = rsa_enc();
|
||||
assert_eq!(k.algorithm.as_str(), "ssh-rsa");
|
||||
assert_eq!(&k.comment, "hello world");
|
||||
|
||||
|
||||
assert_eq!(
|
||||
k.public_key.fingerprint(Default::default()),
|
||||
k.private_key.fingerprint(Default::default()),
|
||||
@ -329,7 +376,7 @@ mod tests {
|
||||
let k = ed25519_plain();
|
||||
assert_eq!(k.algorithm.as_str(),"ssh-ed25519");
|
||||
assert_eq!(&k.comment, "hello world");
|
||||
|
||||
|
||||
assert_eq!(
|
||||
k.public_key.fingerprint(Default::default()),
|
||||
k.private_key.fingerprint(Default::default()),
|
||||
@ -347,7 +394,7 @@ mod tests {
|
||||
let k = ed25519_enc();
|
||||
assert_eq!(k.algorithm.as_str(), "ssh-ed25519");
|
||||
assert_eq!(&k.comment, "hello world");
|
||||
|
||||
|
||||
assert_eq!(
|
||||
k.public_key.fingerprint(Default::default()),
|
||||
k.private_key.fingerprint(Default::default()),
|
||||
@ -399,7 +446,7 @@ mod tests {
|
||||
async fn test_load_db(pool: SqlitePool) {
|
||||
let crypto = Crypto::fixed();
|
||||
let id = uuid!("11111111-1111-1111-1111-111111111111");
|
||||
let k = SshKey::load(&id, &crypto, &pool).await
|
||||
SshKey::load(&id, &crypto, &pool).await
|
||||
.expect("Failed to load SSH key from database");
|
||||
}
|
||||
|
||||
|
@ -195,6 +195,10 @@ pub enum HandlerError {
|
||||
SshAgent(#[from] ssh_agent_lib::error::AgentError),
|
||||
#[error(transparent)]
|
||||
SshKey(#[from] ssh_key::Error),
|
||||
#[error(transparent)]
|
||||
Signature(#[from] signature::Error),
|
||||
#[error(transparent)]
|
||||
Encoding(#[from] ssh_encoding::Error),
|
||||
}
|
||||
|
||||
|
||||
|
@ -142,6 +142,12 @@ pub async fn sshkey_from_file(path: &str, passphrase: &str) -> Result<SshKey, Lo
|
||||
}
|
||||
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn sshkey_from_private_key(private_key: &str, passphrase: &str) -> Result<SshKey, LoadSshKeyError> {
|
||||
SshKey::from_private_key(private_key, passphrase)
|
||||
}
|
||||
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_config(app_state: State<'_, AppState>) -> Result<AppConfig, ()> {
|
||||
let config = app_state.config.read().await;
|
||||
|
@ -44,21 +44,23 @@ pub async fn load_bytes(pool: &SqlitePool, name: &str) -> Result<Option<Vec<u8>>
|
||||
}
|
||||
|
||||
|
||||
// pub async fn delete(pool: &SqlitePool, name: &str) -> Result<(), sqlx::Error> {
|
||||
// sqlx::query!("DELETE FROM kv WHERE name = ?", name)
|
||||
// .execute(pool)
|
||||
// .await?;
|
||||
// Ok(())
|
||||
// }
|
||||
// we don't have a need for this right now, but we will some day
|
||||
#[cfg(test)]
|
||||
pub async fn delete(pool: &SqlitePool, name: &str) -> Result<(), sqlx::Error> {
|
||||
sqlx::query!("DELETE FROM kv WHERE name = ?", name)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
pub async fn delete_multi(pool: &SqlitePool, names: &[&str]) -> Result<(), sqlx::Error> {
|
||||
pub async fn delete_multi(pool: &SqlitePool, names: &[&str]) -> Result<(), sqlx::Error> {
|
||||
let placeholder = names.iter()
|
||||
.map(|_| "?")
|
||||
.collect::<Vec<&str>>()
|
||||
.join(",");
|
||||
let query = format!("DELETE FROM kv WHERE name IN ({})", placeholder);
|
||||
|
||||
|
||||
let mut q = sqlx::query(&query);
|
||||
for name in names {
|
||||
q = q.bind(name);
|
||||
@ -83,7 +85,7 @@ macro_rules! load_bytes_multi {
|
||||
(
|
||||
// ...with one item for each repetition of $name
|
||||
$(
|
||||
// load_bytes returns Result<Option<_>>, the Result is handled by
|
||||
// load_bytes returns Result<Option<_>>, the Result is handled by
|
||||
// the ? and we match on the Option
|
||||
match crate::kv::load_bytes($pool, $name).await? {
|
||||
Some(v) => v,
|
||||
@ -187,7 +189,7 @@ mod tests {
|
||||
async fn test_delete(pool: SqlitePool) {
|
||||
delete(&pool, "test_bytes").await
|
||||
.expect("Failed to delete data");
|
||||
|
||||
|
||||
let loaded = load_bytes(&pool, "test_bytes").await
|
||||
.expect("Failed to load data");
|
||||
assert_eq!(loaded, None);
|
||||
|
@ -11,14 +11,15 @@ use creddy::{
|
||||
|
||||
|
||||
fn main() {
|
||||
let res = match cli::parser().get_matches().subcommand() {
|
||||
let global_matches = cli::parser().get_matches();
|
||||
let res = match global_matches.subcommand() {
|
||||
None | Some(("run", _)) => {
|
||||
app::run().error_popup("Creddy encountered an error");
|
||||
Ok(())
|
||||
},
|
||||
Some(("get", m)) => cli::get(m),
|
||||
Some(("exec", m)) => cli::exec(m),
|
||||
Some(("shortcut", m)) => cli::invoke_shortcut(m),
|
||||
Some(("get", m)) => cli::get(m, &global_matches),
|
||||
Some(("exec", m)) => cli::exec(m, &global_matches),
|
||||
Some(("shortcut", m)) => cli::invoke_shortcut(m, &global_matches),
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
|
@ -1,5 +1,4 @@
|
||||
use futures::SinkExt;
|
||||
use signature::Signer;
|
||||
use ssh_agent_lib::agent::MessageCodec;
|
||||
use ssh_agent_lib::proto::message::{
|
||||
Message,
|
||||
@ -36,12 +35,21 @@ async fn handle(
|
||||
adapter.send(resp).await?;
|
||||
},
|
||||
Message::SignRequest(req) => {
|
||||
// CloseWaiter could corrupt the framing, but this doesn't matter
|
||||
// since we don't plan to pull any more frames out of the stream
|
||||
// Note: If the client writes more data to the stream *while* at the
|
||||
// same time waiting for a resopnse to a previous request, this will
|
||||
// corrupt the framing. Clients don't seem to behave that way though?
|
||||
let waiter = CloseWaiter { stream: adapter.get_mut() };
|
||||
let resp = sign_request(req, app_handle.clone(), client_pid, waiter).await?;
|
||||
|
||||
// have to do this before we send since we can't inspect the message after
|
||||
let is_failure = matches!(resp, Message::Failure);
|
||||
adapter.send(resp).await?;
|
||||
break;
|
||||
|
||||
if is_failure {
|
||||
// this way we don't get spammed with requests for other keys
|
||||
// after denying the first
|
||||
break
|
||||
}
|
||||
},
|
||||
_ => adapter.send(Message::Failure).await?,
|
||||
};
|
||||
@ -93,15 +101,8 @@ async fn sign_request(
|
||||
}
|
||||
|
||||
let key = state.sshkey_by_name(&key_name).await?;
|
||||
let sig = Signer::sign(&key.private_key, &req.data);
|
||||
let key_type = key.algorithm.as_str().as_bytes();
|
||||
|
||||
let payload_len = key_type.len() + sig.as_bytes().len() + 8;
|
||||
let mut payload = Vec::with_capacity(payload_len);
|
||||
encode_string(&mut payload, key.algorithm.as_str().as_bytes());
|
||||
encode_string(&mut payload, sig.as_bytes());
|
||||
|
||||
Ok(Message::SignResponse(payload))
|
||||
let sig = key.sign_request(&req)?;
|
||||
Ok(Message::SignResponse(sig))
|
||||
};
|
||||
|
||||
let res = proceed.await;
|
||||
@ -112,10 +113,3 @@ async fn sign_request(
|
||||
lease.release();
|
||||
res
|
||||
}
|
||||
|
||||
|
||||
fn encode_string(buf: &mut Vec<u8>, s: &[u8]) {
|
||||
let len = s.len() as u32;
|
||||
buf.extend(len.to_be_bytes());
|
||||
buf.extend(s);
|
||||
}
|
||||
|
@ -20,7 +20,7 @@ pub use platform::addr;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub enum Request {
|
||||
GetAwsCredentials {
|
||||
GetAwsCredentials {
|
||||
name: Option<String>,
|
||||
base: bool,
|
||||
},
|
||||
|
@ -50,7 +50,7 @@
|
||||
}
|
||||
},
|
||||
"productName": "creddy",
|
||||
"version": "0.5.0",
|
||||
"version": "0.5.3",
|
||||
"identifier": "creddy",
|
||||
"plugins": {},
|
||||
"app": {
|
||||
@ -85,4 +85,4 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -9,7 +9,7 @@
|
||||
|
||||
|
||||
// Extra 50ms so the window can finish disappearing before the redraw
|
||||
const rehideDelay = Math.min(5000, $appState.config.rehide_ms + 50);
|
||||
const rehideDelay = Math.min(5000, $appState.config.rehide_ms + 100);
|
||||
|
||||
let alert;
|
||||
let success = false;
|
||||
|
@ -15,7 +15,6 @@
|
||||
async function saveCredential() {
|
||||
await invoke('save_credential', {record: local});
|
||||
dispatch('save', local);
|
||||
showDetails = false;
|
||||
}
|
||||
|
||||
async function copyText(evt) {
|
||||
|
@ -13,20 +13,23 @@
|
||||
|
||||
let name;
|
||||
let file;
|
||||
let privateKey = '';
|
||||
let passphrase = '';
|
||||
let showDetails = true;
|
||||
let mode = 'file';
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
let defaultPath = null;
|
||||
homeDir().then(d => defaultPath = `${d}/.ssh`);
|
||||
|
||||
|
||||
let alert;
|
||||
let saving = false;
|
||||
async function saveCredential() {
|
||||
saving = true;
|
||||
try {
|
||||
let key = await invoke('sshkey_from_file', {path: file.path, passphrase});
|
||||
let key = await getKey();
|
||||
const payload = {
|
||||
id: record.id,
|
||||
name,
|
||||
@ -41,9 +44,40 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function getKey() {
|
||||
if (mode === 'file') {
|
||||
return await invoke('sshkey_from_file', {path: file.path, passphrase});
|
||||
}
|
||||
else {
|
||||
return await invoke('sshkey_from_private_key', {privateKey, passphrase});
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
<div role="tablist" class="join max-w-sm mx-auto flex justify-center">
|
||||
<button
|
||||
type="button"
|
||||
role="tab"
|
||||
class="join-item flex-1 btn border border-primary hover:border-primary"
|
||||
class:btn-primary={mode === 'file'}
|
||||
on:click={() => mode = 'file'}
|
||||
>
|
||||
From file
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
role="tab"
|
||||
class="join-item flex-1 btn border border-primary hover:border-primary"
|
||||
class:btn-primary={mode === 'direct'}
|
||||
on:click={() => mode = 'direct'}
|
||||
>
|
||||
From private key
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
||||
<form class="space-y-4" on:submit|preventDefault={alert.run(saveCredential)}>
|
||||
<ErrorAlert bind:this={alert} />
|
||||
|
||||
@ -55,15 +89,20 @@
|
||||
bind:value={name}
|
||||
>
|
||||
|
||||
<span class="justify-self-end">File</span>
|
||||
<FileInput params={{defaultPath}} bind:value={file} on:update={() => name = file.name} />
|
||||
{#if mode === 'file'}
|
||||
<span class="justify-self-end">File</span>
|
||||
<FileInput params={{defaultPath}} bind:value={file} on:update={() => name = file.name} />
|
||||
{:else}
|
||||
<span class="justify-self-end">Private key</span>
|
||||
<textarea bind:value={privateKey} rows="5" class="textarea textarea-bordered bg-transparent font-mono whitespace-pre overflow-x-auto"></textarea>
|
||||
{/if}
|
||||
|
||||
<span class="justify-self-end">Passphrase</span>
|
||||
<PassphraseInput class="bg-transparent" bind:value={passphrase} />
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end">
|
||||
{#if file?.path}
|
||||
{#if file?.path || privateKey !== ''}
|
||||
<button
|
||||
transition:fade={{duration: 100}}
|
||||
type="submit"
|
||||
|
@ -64,7 +64,7 @@
|
||||
{#if record.isNew}
|
||||
<NewSshKey {record} on:save on:save={handleSave} />
|
||||
{:else}
|
||||
<EditSshKey bind:local={local} {isModified} on:save />
|
||||
<EditSshKey bind:local={local} {isModified} on:save={handleSave} on:save />
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
Reference in New Issue
Block a user