1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
|
use std::path::Path;
use crate::base64_decode;
/// An error that can occur when analyzing SSH keys.
#[derive(Debug)]
pub enum Error {
/// Failed to open the key file.
OpenFile(std::io::Error),
/// Failed to read from the key file.
ReadFile(std::io::Error),
/// Missing PEM trailer in the file (there was a PEM header).
MissingPemTrailer,
/// The key is not valid somehow.
MalformedKey,
/// There was an invalid base64 blob in the key.
Base64(base64_decode::Error),
}
/// The format of a key file.
pub enum KeyFormat {
/// We don't know what format it is.
Unknown,
/// It's an openssh-key-v1 file.
///
/// See https://coolaj86.com/articles/the-openssh-private-key-format/ for a description of the format.
OpensshKeyV1,
}
/// Information about a key file.
pub struct KeyInfo {
/// The format of the key file.
pub format: KeyFormat,
/// Is the key encrypted?
pub encrypted: bool,
}
/// Analyze an SSH key file.
pub fn analyze_ssh_key_file(priv_key_path: &Path) -> Result<KeyInfo, Error> {
use std::io::Read;
let mut buffer = Vec::new();
let mut file = std::fs::File::open(priv_key_path)
.map_err(Error::OpenFile)?;
file.read_to_end(&mut buffer)
.map_err(Error::ReadFile)?;
analyze_pem_openssh_key(&buffer)
}
/// Analyze a PEM encoded openssh-key-v1 file.
fn analyze_pem_openssh_key(data: &[u8]) -> Result<KeyInfo, Error> {
let data = trim_bytes(data);
let data = match data.strip_prefix(b"-----BEGIN OPENSSH PRIVATE KEY-----") {
Some(x) => x,
None => return Ok(KeyInfo { format: KeyFormat::Unknown, encrypted: false }),
};
let data = match data.strip_suffix(b"-----END OPENSSH PRIVATE KEY-----") {
Some(x) => x,
None => return Err(Error::MissingPemTrailer),
};
let data = base64_decode::base64_decode(data).map_err(Error::Base64)?;
analyze_binary_openssh_key(&data)
}
/// Analyze a binary openss-key-v1 blob.
fn analyze_binary_openssh_key(data: &[u8]) -> Result<KeyInfo, Error> {
let tail = data.strip_prefix(b"openssh-key-v1\0")
.ok_or(Error::MalformedKey)?;
if tail.len() <= 4 {
return Err(Error::MalformedKey);
}
let (cipher_len, tail) = tail.split_at(4);
let cipher_len = u32::from_be_bytes(cipher_len.try_into().unwrap()) as usize;
if tail.len() < cipher_len {
return Err(Error::MalformedKey);
}
let cipher = &tail[..cipher_len];
let encrypted = cipher != b"none";
Ok(KeyInfo { format: KeyFormat::OpensshKeyV1, encrypted })
}
/// Trim whitespace from the start and end of a byte slice.
fn trim_bytes(data: &[u8]) -> &[u8] {
let data = match data.iter().position(|b| !b.is_ascii_whitespace()) {
Some(x) => &data[x..],
None => return b"",
};
let data = match data.iter().rposition(|b| !b.is_ascii_whitespace()) {
Some(x) => &data[..=x],
None => return b"",
};
data
}
impl std::fmt::Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::OpenFile(e) => write!(f, "Failed to open file: {e}"),
Self::ReadFile(e) => write!(f, "Failed to read from file: {e}"),
Self::MissingPemTrailer => write!(f, "Missing PEM trailer in key file"),
Self::MalformedKey => write!(f, "Invalid or malformed key file"),
Self::Base64(e) => write!(f, "Invalid base64 in key file: {e}"),
}
}
}
#[cfg(test)]
mod test {
use super::*;
use assert2::assert;
#[test]
fn test_is_encrypted_pem_openssh_key() {
// Encrypted OpenSSH key.
assert!(let Ok(KeyInfo { format: KeyFormat::OpensshKeyV1, encrypted: true }) = analyze_pem_openssh_key(concat!(
"-----BEGIN OPENSSH PRIVATE KEY-----\n",
"b3BlbnNzaC1rZXktdjEAAAAACmFlczI1Ni1jdHIAAAAGYmNyeXB0AAAAGAAAABBddrJWnj\n",
"6eysG+DqTberHEAAAAEAAAAAEAAAAzAAAAC3NzaC1lZDI1NTE5AAAAIARNG0xAyCq6/OFQ\n",
"8eQFG1zKYlhtLLz2GC3Sou+C9PTmAAAAoGPGz6ZQhBk8FL4MRDaGsaZuVkPAn/+curIR7r\n",
"rDoXPAf0/7S2dVWY0gUjolhwlqGFnps4NgukXtKNs4qlAJiVAY/kKPr0fN+ZScuNuKP/Im\n",
"JbFoNPRaakzgbBwj9/UTpwNgUJa+3fu25l1RMLlrx7OjkQKAHBb6VMsGqH8k9rAEsCCBUK\n",
"XVJQOMAfa214eo9wgHD06ZnIlk3jS++3hzyUs=\n",
"-----END OPENSSH PRIVATE KEY-----\n",
).as_bytes()));
// Encrypted OpenSSH key with extra random whitespace.
assert!(let Ok(KeyInfo { format: KeyFormat::OpensshKeyV1, encrypted: true }) = analyze_pem_openssh_key(concat!(
" \n\t\r-----BEGIN OPENSSH PRIVATE KEY-----\n",
"b3BlbnNzaC1rZXktdjEAAAAACmFlczI1Ni1jdHIAAAAGYmNyeXB0AAAAGAAAABBddrJWnj\n",
"6eysG+DqTberHEAAAAEAAAAAEAAAAzAAAAC3NzaC1lZDI1NTE5AAAAIARNG0xAyCq6/OFQ\n \r",
"8eQFG1zKYlhtLLz2GC3Sou+ C9PTmAAAAoGPGz6ZQhBk8FL4MRDaGsaZuVkPAn/+curIR7r\n",
"rDoXPAf0/7S2dVWY0gUjolhwlqGFnps4NgukXtKNs4qlAJiVAY/kKPr0fN+ZScuNuKP/Im\n",
"JbFoNPRaakzgbBwj9/UTpwNgUJa+3fu25l1RMLlrx7OjkQKAHBb6VMsGqH8k9rAEsCCBUK\n",
"XVJQOMAfa214eo9wgHD06ZnIlk3jS++3hzyUs=\n",
"-----END OPENSSH PRIVATE KEY-----",
).as_bytes()));
// Unencrypted OpenSSH key.
assert!(let Ok(KeyInfo { format: KeyFormat::OpensshKeyV1, encrypted: false }) = analyze_pem_openssh_key(concat!(
"-----BEGIN OPENSSH PRIVATE KEY-----\n",
"b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW\n",
"QyNTUxOQAAACDTKM0+RYzELoLewv5n5UoEPhmCpwkrtXM4GpWUVF+w3AAAAJhSNRa9UjUW\n",
"vQAAAAtzc2gtZWQyNTUxOQAAACDTKM0+RYzELoLewv5n5UoEPhmCpwkrtXM4GpWUVF+w3A\n",
"AAAECZObXz1xTSvl4vpLsMVTuhjroyDteKlW+Uun0yIMl7edMozT5FjMQugt7C/mflSgQ+\n",
"GYKnCSu1czgalZRUX7DcAAAAEW1hYXJ0ZW5AbWFnbmV0cm9uAQIDBA==\n",
"-----END OPENSSH PRIVATE KEY-----\n",
).as_bytes()));
}
}
|