Skip to content

Commit dfdc464

Browse files
Add pwdf retry logic to OpenSSHKeyV1KeyFile (#587)
* Add pwdf retry logic to OpenSSHKeyV1KeyFile While PKCS8KeyFile uses PasswordFinder's shouldRetry to determine whether it should call reqPassword again if decryption of they key file fails, OpenSSHKeyV1KeyFile simply gives up and throws an exception. With this commit, retry logic similar to that of PKCS8KeyFile is added to OpenSSHKeyV1KeyFile. The PasswordFinder's reqPassword is called again if the validation of the "checkint" fails, which indicates an incorrect passphrase. * Use new exception to signal incorrect passphrase * Throw common exception on key decryption failure * Add test coverage for retry logic Co-authored-by: Jeroen van Erp <jeroen@hierynomus.com>
1 parent fa7c40c commit dfdc464

File tree

4 files changed

+88
-9
lines changed

4 files changed

+88
-9
lines changed
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/*
2+
* Copyright (C)2009 - SSHJ Contributors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.hierynomus.sshj.common;
17+
18+
import org.bouncycastle.openssl.EncryptionException;
19+
20+
import java.io.IOException;
21+
22+
/**
23+
* Thrown when a key file could not be decrypted correctly, e.g. if its checkInts differed in the case of an OpenSSH
24+
* key file.
25+
*/
26+
public class KeyDecryptionFailedException extends IOException {
27+
28+
public static final String MESSAGE = "Decryption of the key failed. A supplied passphrase may be incorrect.";
29+
30+
public KeyDecryptionFailedException() {
31+
super(MESSAGE);
32+
}
33+
34+
public KeyDecryptionFailedException(EncryptionException cause) {
35+
super(MESSAGE, cause);
36+
}
37+
38+
}

src/main/java/com/hierynomus/sshj/userauth/keyprovider/OpenSSHKeyV1KeyFile.java

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
*/
1616
package com.hierynomus.sshj.userauth.keyprovider;
1717

18+
import com.hierynomus.sshj.common.KeyDecryptionFailedException;
1819
import com.hierynomus.sshj.transport.cipher.BlockCiphers;
1920
import net.i2p.crypto.eddsa.EdDSAPrivateKey;
2021
import net.i2p.crypto.eddsa.spec.EdDSANamedCurveTable;
@@ -111,8 +112,16 @@ private KeyPair readDecodedKeyPair(final PlainBuffer keyBuffer) throws IOExcepti
111112
return readUnencrypted(privateKeyBuffer, publicKey);
112113
} else {
113114
logger.info("Keypair is encrypted with: " + cipherName + ", " + kdfName + ", " + Arrays.toString(kdfOptions));
114-
PlainBuffer decrypted = decryptBuffer(privateKeyBuffer, cipherName, kdfName, kdfOptions);
115-
return readUnencrypted(decrypted, publicKey);
115+
while (true) {
116+
PlainBuffer decryptionBuffer = new PlainBuffer(privateKeyBuffer);
117+
PlainBuffer decrypted = decryptBuffer(decryptionBuffer, cipherName, kdfName, kdfOptions);
118+
try {
119+
return readUnencrypted(decrypted, publicKey);
120+
} catch (KeyDecryptionFailedException e) {
121+
if (pwdf == null || !pwdf.shouldRetry(resource))
122+
throw e;
123+
}
124+
}
116125
// throw new IOException("Cannot read encrypted keypair with " + cipherName + " yet.");
117126
}
118127
}
@@ -184,7 +193,7 @@ private KeyPair readUnencrypted(final PlainBuffer keyBuffer, final PublicKey pub
184193
int checkInt1 = keyBuffer.readUInt32AsInt(); // uint32 checkint1
185194
int checkInt2 = keyBuffer.readUInt32AsInt(); // uint32 checkint2
186195
if (checkInt1 != checkInt2) {
187-
throw new IOException("The checkInts differed, the key was not correctly decoded.");
196+
throw new KeyDecryptionFailedException();
188197
}
189198
// The private key section contains both the public key and the private key
190199
String keyType = keyBuffer.readString(); // string keytype

src/main/java/net/schmizz/sshj/userauth/keyprovider/PKCS8KeyFile.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
*/
1616
package net.schmizz.sshj.userauth.keyprovider;
1717

18+
import com.hierynomus.sshj.common.KeyDecryptionFailedException;
1819
import net.schmizz.sshj.common.IOUtils;
1920
import net.schmizz.sshj.common.SecurityUtils;
2021
import net.schmizz.sshj.userauth.password.PasswordUtils;
@@ -85,7 +86,7 @@ protected KeyPair readKeyPair()
8586
if (pwdf != null && pwdf.shouldRetry(resource))
8687
continue;
8788
else
88-
throw e;
89+
throw new KeyDecryptionFailedException(e);
8990
} finally {
9091
IOUtils.closeQuietly(r);
9192
}

src/test/java/net/schmizz/sshj/keyprovider/OpenSSHKeyFileTest.java

Lines changed: 36 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
*/
1616
package net.schmizz.sshj.keyprovider;
1717

18+
import com.hierynomus.sshj.common.KeyDecryptionFailedException;
1819
import com.hierynomus.sshj.userauth.certificate.Certificate;
1920
import com.hierynomus.sshj.userauth.keyprovider.OpenSSHKeyV1KeyFile;
2021
import net.schmizz.sshj.common.KeyType;
@@ -200,12 +201,34 @@ public void shouldLoadED25519PrivateKey() throws IOException {
200201

201202
@Test
202203
public void shouldLoadProtectedED25519PrivateKeyAes256CTR() throws IOException {
203-
checkOpenSSHKeyV1("src/test/resources/keytypes/ed25519_protected", "sshjtest");
204+
checkOpenSSHKeyV1("src/test/resources/keytypes/ed25519_protected", "sshjtest", false);
205+
checkOpenSSHKeyV1("src/test/resources/keytypes/ed25519_protected", "sshjtest", true);
204206
}
205207

206208
@Test
207209
public void shouldLoadProtectedED25519PrivateKeyAes256CBC() throws IOException {
208-
checkOpenSSHKeyV1("src/test/resources/keytypes/ed25519_aes256cbc.pem", "foobar");
210+
checkOpenSSHKeyV1("src/test/resources/keytypes/ed25519_aes256cbc.pem", "foobar", false);
211+
checkOpenSSHKeyV1("src/test/resources/keytypes/ed25519_aes256cbc.pem", "foobar", true);
212+
}
213+
214+
@Test(expected = KeyDecryptionFailedException.class)
215+
public void shouldFailOnIncorrectPassphraseAfterRetries() throws IOException {
216+
OpenSSHKeyV1KeyFile keyFile = new OpenSSHKeyV1KeyFile();
217+
keyFile.init(new File("src/test/resources/keytypes/ed25519_aes256cbc.pem"), new PasswordFinder() {
218+
private int reqCounter = 0;
219+
220+
@Override
221+
public char[] reqPassword(Resource<?> resource) {
222+
reqCounter++;
223+
return "incorrect".toCharArray();
224+
}
225+
226+
@Override
227+
public boolean shouldRetry(Resource<?> resource) {
228+
return reqCounter <= 3;
229+
}
230+
});
231+
keyFile.getPrivate();
209232
}
210233

211234
@Test
@@ -224,17 +247,25 @@ public void shouldLoadECDSAPrivateKeyAsOpenSSHV1() throws IOException {
224247
assertThat(aPrivate.getAlgorithm(), equalTo("ECDSA"));
225248
}
226249

227-
private void checkOpenSSHKeyV1(String key, final String password) throws IOException {
250+
private void checkOpenSSHKeyV1(String key, final String password, boolean withRetry) throws IOException {
228251
OpenSSHKeyV1KeyFile keyFile = new OpenSSHKeyV1KeyFile();
229252
keyFile.init(new File(key), new PasswordFinder() {
253+
private int reqCounter = 0;
254+
230255
@Override
231256
public char[] reqPassword(Resource<?> resource) {
232-
return password.toCharArray();
257+
if (withRetry && reqCounter < 3) {
258+
reqCounter++;
259+
// Return an incorrect password three times before returning the correct one.
260+
return (password + "incorrect").toCharArray();
261+
} else {
262+
return password.toCharArray();
263+
}
233264
}
234265

235266
@Override
236267
public boolean shouldRetry(Resource<?> resource) {
237-
return false;
268+
return withRetry && reqCounter <= 3;
238269
}
239270
});
240271
PrivateKey aPrivate = keyFile.getPrivate();

0 commit comments

Comments
 (0)