@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.)
hq.recaptime.dev/wiki/Phorge
phorge
phabricator
1<?php
2
3/**
4 * Data structure representing a raw private key.
5 */
6final 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}