Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
freebsd
GitHub Repository: freebsd/phabricator
Path: blob/master/src/applications/auth/sshkey/PhabricatorAuthSSHPrivateKey.php
12256 views
1
<?php
2
3
/**
4
* Data structure representing a raw private key.
5
*/
6
final class PhabricatorAuthSSHPrivateKey extends Phobject {
7
8
private $body;
9
private $passphrase;
10
11
private function __construct() {
12
// <internal>
13
}
14
15
public function setPassphrase(PhutilOpaqueEnvelope $passphrase) {
16
$this->passphrase = $passphrase;
17
return $this;
18
}
19
20
public function getPassphrase() {
21
return $this->passphrase;
22
}
23
24
public static function newFromRawKey(PhutilOpaqueEnvelope $entire_key) {
25
$key = new self();
26
27
$key->body = $entire_key;
28
29
return $key;
30
}
31
32
public function getKeyBody() {
33
return $this->body;
34
}
35
36
public function newBarePrivateKey() {
37
if (!Filesystem::binaryExists('ssh-keygen')) {
38
throw new Exception(
39
pht(
40
'Analyzing or decrypting SSH keys requires the "ssh-keygen" binary, '.
41
'but it is not available in "$PATH". Make it available to work with '.
42
'SSH private keys.'));
43
}
44
45
$old_body = $this->body;
46
47
// Some versions of "ssh-keygen" are sensitive to trailing whitespace for
48
// some keys. Trim any trailing whitespace and replace it with a single
49
// newline.
50
$raw_body = $old_body->openEnvelope();
51
$raw_body = rtrim($raw_body)."\n";
52
$old_body = new PhutilOpaqueEnvelope($raw_body);
53
54
$tmp = $this->newTemporaryPrivateKeyFile($old_body);
55
56
// See T13454 for discussion of why this is so awkward. In broad strokes,
57
// we don't have a straightforward way to distinguish between keys with an
58
// invalid format and keys with a passphrase which we don't know.
59
60
// First, try to extract the public key from the file using the (possibly
61
// empty) passphrase we were given. If everything is in good shape, this
62
// should work.
63
64
$passphrase = $this->getPassphrase();
65
if ($passphrase) {
66
list($err, $stdout, $stderr) = exec_manual(
67
'ssh-keygen -y -P %P -f %R',
68
$passphrase,
69
$tmp);
70
} else {
71
list($err, $stdout, $stderr) = exec_manual(
72
'ssh-keygen -y -P %s -f %R',
73
'',
74
$tmp);
75
}
76
77
// If that worked, the key is good and the (possibly empty) passphrase is
78
// correct. Strip the passphrase if we have one, then return the bare key.
79
80
if (!$err) {
81
if ($passphrase) {
82
execx(
83
'ssh-keygen -p -P %P -N %s -f %R',
84
$passphrase,
85
'',
86
$tmp);
87
88
$new_body = new PhutilOpaqueEnvelope(Filesystem::readFile($tmp));
89
unset($tmp);
90
} else {
91
$new_body = $old_body;
92
}
93
94
return self::newFromRawKey($new_body);
95
}
96
97
// We were not able to extract the public key. Try to figure out why. The
98
// reasons we expect are:
99
//
100
// - We were given a passphrase, but the key has no passphrase.
101
// - We were given a passphrase, but the passphrase is wrong.
102
// - We were not given a passphrase, but the key has a passphrase.
103
// - The key format is invalid.
104
//
105
// Our ability to separate these cases varies a lot, particularly because
106
// some versions of "ssh-keygen" return very similar diagnostic messages
107
// for any error condition. Try our best.
108
109
if ($passphrase) {
110
// First, test for "we were given a passphrase, but the key has no
111
// passphrase", since this is a conclusive test.
112
list($err) = exec_manual(
113
'ssh-keygen -y -P %s -f %R',
114
'',
115
$tmp);
116
if (!$err) {
117
throw new PhabricatorAuthSSHPrivateKeySurplusPassphraseException(
118
pht(
119
'A passphrase was provided for this private key, but it does '.
120
'not require a passphrase. Check that you supplied the correct '.
121
'key, or omit the passphrase.'));
122
}
123
}
124
125
// We're out of conclusive tests, so try to guess why the error occurred.
126
// In some versions of "ssh-keygen", we get a usable diagnostic message. In
127
// other versions, not so much.
128
129
$reason_format = 'format';
130
$reason_passphrase = 'passphrase';
131
$reason_unknown = 'unknown';
132
133
$patterns = array(
134
// macOS 10.14.6
135
'/incorrect passphrase supplied to decrypt private key/'
136
=> $reason_passphrase,
137
138
// macOS 10.14.6
139
'/invalid format/' => $reason_format,
140
141
// Ubuntu 14
142
'/load failed/' => $reason_unknown,
143
);
144
145
$reason = 'unknown';
146
foreach ($patterns as $pattern => $pattern_reason) {
147
$ok = preg_match($pattern, $stderr);
148
149
if ($ok === false) {
150
throw new Exception(
151
pht(
152
'Pattern "%s" is not valid.',
153
$pattern));
154
}
155
156
if ($ok) {
157
$reason = $pattern_reason;
158
break;
159
}
160
}
161
162
if ($reason === $reason_format) {
163
throw new PhabricatorAuthSSHPrivateKeyFormatException(
164
pht(
165
'This private key is not formatted correctly. Check that you '.
166
'have provided the complete text of a valid private key.'));
167
}
168
169
if ($reason === $reason_passphrase) {
170
if ($passphrase) {
171
throw new PhabricatorAuthSSHPrivateKeyIncorrectPassphraseException(
172
pht(
173
'This private key requires a passphrase, but the wrong '.
174
'passphrase was provided. Check that you supplied the correct '.
175
'key and passphrase.'));
176
} else {
177
throw new PhabricatorAuthSSHPrivateKeyIncorrectPassphraseException(
178
pht(
179
'This private key requires a passphrase, but no passphrase was '.
180
'provided. Check that you supplied the correct key, or provide '.
181
'the passphrase.'));
182
}
183
}
184
185
if ($passphrase) {
186
throw new PhabricatorAuthSSHPrivateKeyUnknownException(
187
pht(
188
'This private key could not be opened with the provided passphrase. '.
189
'This might mean that the passphrase is wrong or that the key is '.
190
'not formatted correctly. Check that you have supplied the '.
191
'complete text of a valid private key and the correct passphrase.'));
192
} else {
193
throw new PhabricatorAuthSSHPrivateKeyUnknownException(
194
pht(
195
'This private key could not be opened. This might mean that the '.
196
'key requires a passphrase, or might mean that the key is not '.
197
'formatted correctly. Check that you have supplied the complete '.
198
'text of a valid private key and the correct passphrase.'));
199
}
200
}
201
202
private function newTemporaryPrivateKeyFile(PhutilOpaqueEnvelope $key_body) {
203
$tmp = new TempFile();
204
205
Filesystem::writeFile($tmp, $key_body->openEnvelope());
206
207
return $tmp;
208
}
209
210
}
211
212