Enable SSH remotes in native frontends (#117)
This commit is contained in:
1 parent
76e707f645
commit
a737e74aae
22 files changed
+1214
-140
No files matched your search
+131
-3
@@ -5,11 +5,13 @@ use std::{fmt, num::NonZeroUsize};
|
||||
use crossterm::event::{KeyCode, KeyModifiers};
|
||||
use ironstorage::{
|
||||
command::{
|
||||
CopyRequest, GenerateRequest, GeneratedPresentation, GrepRequest, InitRequest, InsertInput,
|
||||
InsertRequest, MoveRequest, OtpAppendRequest, OtpInputSource, OtpInsertRequest,
|
||||
RemoveRequest,
|
||||
CopyRequest, GenerateRequest, GeneratedPresentation, GitRequest, GrepRequest, InitRequest,
|
||||
InsertInput, InsertRequest, MoveRequest, OtpAppendRequest, OtpInputSource,
|
||||
OtpInsertRequest, RemoveRequest,
|
||||
},
|
||||
config::SshFingerprint,
|
||||
crypto::KeyInfo,
|
||||
git::SshHostKey,
|
||||
kdbx::{KdbxImportMode, KdbxImportRequest},
|
||||
otp::OtpInput,
|
||||
repository::SecretBytes,
|
||||
@@ -167,6 +169,20 @@ pub struct OtpForm {
|
||||
focus: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct SshHostForm {
|
||||
request: GitRequest,
|
||||
host_key: SshHostKey,
|
||||
confirmed: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct SshPassphraseForm {
|
||||
request: GitRequest,
|
||||
fingerprint: SshFingerprint,
|
||||
passphrase: SecretText,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum WorkflowForm {
|
||||
Init(InitForm),
|
||||
@@ -178,6 +194,8 @@ pub enum WorkflowForm {
|
||||
Move(TransferForm),
|
||||
Copy(TransferForm),
|
||||
Otp(OtpForm),
|
||||
SshHost(SshHostForm),
|
||||
SshPassphrase(SshPassphraseForm),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -217,6 +235,15 @@ pub enum WorkflowSubmission {
|
||||
OtpValidate {
|
||||
uri: OtpInput,
|
||||
},
|
||||
SshHost {
|
||||
request: GitRequest,
|
||||
host_key: SshHostKey,
|
||||
},
|
||||
SshPassphrase {
|
||||
request: GitRequest,
|
||||
fingerprint: SshFingerprint,
|
||||
passphrase: SecretBytes,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
@@ -228,6 +255,22 @@ pub enum WorkflowInput {
|
||||
}
|
||||
|
||||
impl WorkflowForm {
|
||||
pub fn ssh_host(request: GitRequest, host_key: SshHostKey) -> Self {
|
||||
Self::SshHost(SshHostForm {
|
||||
request,
|
||||
host_key,
|
||||
confirmed: false,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn ssh_passphrase(request: GitRequest, fingerprint: SshFingerprint) -> Self {
|
||||
Self::SshPassphrase(SshPassphraseForm {
|
||||
request,
|
||||
fingerprint,
|
||||
passphrase: SecretText::default(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn otp(kind: OtpFormKind, entry: Option<String>, force: bool) -> Self {
|
||||
Self::Otp(OtpForm {
|
||||
kind,
|
||||
@@ -409,6 +452,8 @@ impl WorkflowForm {
|
||||
OtpFormKind::Append => "Append OTP URI",
|
||||
OtpFormKind::Validate => "Validate OTP URI",
|
||||
},
|
||||
Self::SshHost(_) => "Confirm SSH host key",
|
||||
Self::SshPassphrase(_) => "Unlock SSH private key",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -579,6 +624,28 @@ impl WorkflowForm {
|
||||
),
|
||||
]
|
||||
}
|
||||
Self::SshHost(form) => vec![
|
||||
row(
|
||||
false,
|
||||
"Host",
|
||||
&format!("{}:{}", form.host_key.host(), form.host_key.port()),
|
||||
),
|
||||
row(false, "Algorithm", form.host_key.algorithm()),
|
||||
row(false, "Fingerprint", form.host_key.fingerprint().as_str()),
|
||||
row(true, "Trust this observed key", yes_no(form.confirmed)),
|
||||
],
|
||||
Self::SshPassphrase(form) => vec![
|
||||
row(false, "Key fingerprint", form.fingerprint.as_str()),
|
||||
row(
|
||||
true,
|
||||
"Passphrase",
|
||||
if form.passphrase.is_empty() {
|
||||
"(empty)"
|
||||
} else {
|
||||
"••••••••"
|
||||
},
|
||||
),
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -789,6 +856,25 @@ impl WorkflowForm {
|
||||
}
|
||||
}
|
||||
}
|
||||
Self::SshHost(form) => {
|
||||
if !form.confirmed {
|
||||
return Err("Explicitly confirm the verified SSH host fingerprint".to_owned());
|
||||
}
|
||||
Ok(WorkflowSubmission::SshHost {
|
||||
request: form.request.clone(),
|
||||
host_key: form.host_key.clone(),
|
||||
})
|
||||
}
|
||||
Self::SshPassphrase(form) => {
|
||||
if form.passphrase.is_empty() {
|
||||
return Err("SSH key passphrase is required".to_owned());
|
||||
}
|
||||
Ok(WorkflowSubmission::SshPassphrase {
|
||||
request: form.request.clone(),
|
||||
fingerprint: form.fingerprint.clone(),
|
||||
passphrase: SecretBytes::new(form.passphrase.bytes()),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -801,6 +887,7 @@ impl WorkflowForm {
|
||||
Self::Kdbx(_) => 5,
|
||||
Self::Remove(_) | Self::Move(_) | Self::Copy(_) => 4,
|
||||
Self::Otp(_) => 4,
|
||||
Self::SshHost(_) | Self::SshPassphrase(_) => 1,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -815,6 +902,7 @@ impl WorkflowForm {
|
||||
Self::Remove(form) => &mut form.focus,
|
||||
Self::Move(form) | Self::Copy(form) => &mut form.focus,
|
||||
Self::Otp(form) => &mut form.focus,
|
||||
Self::SshHost(_) | Self::SshPassphrase(_) => return,
|
||||
};
|
||||
*focus = if forward {
|
||||
(*focus + 1) % count
|
||||
@@ -863,6 +951,7 @@ impl WorkflowForm {
|
||||
Self::Move(form) | Self::Copy(form) if form.focus == 3 => form.overwrite ^= true,
|
||||
Self::Otp(form) if form.focus == 2 => form.force ^= true,
|
||||
Self::Otp(form) if form.focus == 3 => form.confirmed ^= true,
|
||||
Self::SshHost(form) => form.confirmed ^= true,
|
||||
_ => self.character(' '),
|
||||
}
|
||||
}
|
||||
@@ -919,6 +1008,7 @@ impl WorkflowForm {
|
||||
Self::Otp(form) if form.focus == 1 => {
|
||||
form.uri.pop();
|
||||
}
|
||||
Self::SshPassphrase(form) => form.passphrase.pop(),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
@@ -954,6 +1044,7 @@ impl WorkflowForm {
|
||||
}
|
||||
Self::Otp(form) if form.focus == 0 => form.entry.push(character),
|
||||
Self::Otp(form) if form.focus == 1 && character != '\n' => form.uri.push(character),
|
||||
Self::SshPassphrase(form) if character != '\n' => form.passphrase.push(character),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
@@ -1052,6 +1143,43 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ssh_passphrase_prompt_is_masked_and_returns_typed_secret_input() {
|
||||
let fingerprint = ironstorage::config::SshFingerprint::parse(
|
||||
"SHA256:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
|
||||
)
|
||||
.expect("fingerprint");
|
||||
let mut form = WorkflowForm::ssh_passphrase(
|
||||
ironstorage::command::GitRequest::Sync {
|
||||
remote: Some("origin".to_owned()),
|
||||
},
|
||||
fingerprint.clone(),
|
||||
);
|
||||
type_text(&mut form, "terminal secret");
|
||||
let rendered = format!("{:?} {:?}", form.title(), form.rows());
|
||||
assert!(rendered.contains(fingerprint.as_str()));
|
||||
assert!(rendered.contains("••••••••"));
|
||||
assert!(!rendered.contains("terminal secret"));
|
||||
let debug = format!("{form:?}");
|
||||
assert!(!debug.contains("terminal secret"));
|
||||
let WorkflowSubmission::SshPassphrase {
|
||||
request,
|
||||
fingerprint: submitted,
|
||||
passphrase,
|
||||
} = form.submission().expect("submission")
|
||||
else {
|
||||
panic!("SSH passphrase submission")
|
||||
};
|
||||
assert_eq!(
|
||||
request,
|
||||
ironstorage::command::GitRequest::Sync {
|
||||
remote: Some("origin".to_owned())
|
||||
}
|
||||
);
|
||||
assert_eq!(submitted, fingerprint);
|
||||
assert_eq!(passphrase.expose(), b"terminal secret");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hidden_entry_is_confirmed_and_never_rendered_or_debugged() {
|
||||
let mut form = WorkflowForm::insert(None);
|
||||
|
||||
Reference in new issue
Block a user