|
18 | 18 | import com.hierynomus.sshj.common.KeyAlgorithm; |
19 | 19 | import com.hierynomus.sshj.common.KeyDecryptionFailedException; |
20 | 20 | import com.hierynomus.sshj.transport.cipher.BlockCiphers; |
| 21 | +import com.hierynomus.sshj.transport.cipher.GcmCiphers; |
21 | 22 | import net.i2p.crypto.eddsa.EdDSAPrivateKey; |
22 | 23 | import net.i2p.crypto.eddsa.spec.EdDSANamedCurveTable; |
23 | 24 | import net.i2p.crypto.eddsa.spec.EdDSAPrivateKeySpec; |
|
36 | 37 | import org.bouncycastle.asn1.x9.X9ECParameters; |
37 | 38 | import org.bouncycastle.jce.spec.ECNamedCurveSpec; |
38 | 39 | import com.hierynomus.sshj.userauth.keyprovider.bcrypt.BCrypt; |
| 40 | +import org.bouncycastle.openssl.EncryptionException; |
39 | 41 | import org.slf4j.Logger; |
40 | 42 | import org.slf4j.LoggerFactory; |
41 | 43 |
|
|
47 | 49 | import java.math.BigInteger; |
48 | 50 | import java.nio.ByteBuffer; |
49 | 51 | import java.nio.CharBuffer; |
50 | | -import java.nio.charset.Charset; |
| 52 | +import java.nio.charset.StandardCharsets; |
51 | 53 | import java.security.*; |
52 | 54 | import java.security.spec.ECPrivateKeySpec; |
53 | 55 | import java.security.spec.RSAPrivateCrtKeySpec; |
54 | 56 | import java.util.Arrays; |
55 | 57 | import java.util.Base64; |
| 58 | +import java.util.HashMap; |
| 59 | +import java.util.Map; |
56 | 60 |
|
57 | 61 | /** |
58 | 62 | * Reads a key file in the new OpenSSH format. |
59 | 63 | * The format is described in the following document: https://github.com/openssh/openssh-portable/blob/master/PROTOCOL.key |
60 | 64 | */ |
61 | 65 | public class OpenSSHKeyV1KeyFile extends BaseFileKeyProvider { |
62 | | - private static final Logger logger = LoggerFactory.getLogger(OpenSSHKeyV1KeyFile.class); |
63 | 66 | private static final String BEGIN = "-----BEGIN "; |
64 | 67 | private static final String END = "-----END "; |
65 | 68 | private static final byte[] AUTH_MAGIC = "openssh-key-v1\0".getBytes(); |
66 | 69 | public static final String OPENSSH_PRIVATE_KEY = "OPENSSH PRIVATE KEY-----"; |
67 | 70 | public static final String BCRYPT = "bcrypt"; |
| 71 | + |
| 72 | + private static final String NONE_CIPHER = "none"; |
| 73 | + |
| 74 | + private static final Map<String, Factory.Named<Cipher>> SUPPORTED_CIPHERS = new HashMap<>(); |
| 75 | + |
| 76 | + static { |
| 77 | + SUPPORTED_CIPHERS.put(BlockCiphers.TripleDESCBC().getName(), BlockCiphers.TripleDESCBC()); |
| 78 | + SUPPORTED_CIPHERS.put(BlockCiphers.AES128CBC().getName(), BlockCiphers.AES128CBC()); |
| 79 | + SUPPORTED_CIPHERS.put(BlockCiphers.AES192CBC().getName(), BlockCiphers.AES192CBC()); |
| 80 | + SUPPORTED_CIPHERS.put(BlockCiphers.AES256CBC().getName(), BlockCiphers.AES256CBC()); |
| 81 | + SUPPORTED_CIPHERS.put(BlockCiphers.AES128CTR().getName(), BlockCiphers.AES128CTR()); |
| 82 | + SUPPORTED_CIPHERS.put(BlockCiphers.AES192CTR().getName(), BlockCiphers.AES192CTR()); |
| 83 | + SUPPORTED_CIPHERS.put(BlockCiphers.AES256CTR().getName(), BlockCiphers.AES256CTR()); |
| 84 | + SUPPORTED_CIPHERS.put(GcmCiphers.AES256GCM().getName(), GcmCiphers.AES256GCM()); |
| 85 | + SUPPORTED_CIPHERS.put(GcmCiphers.AES128GCM().getName(), GcmCiphers.AES128GCM()); |
| 86 | + } |
| 87 | + |
68 | 88 | private PublicKey pubKey; |
69 | 89 |
|
70 | 90 | public static class Factory |
@@ -135,74 +155,117 @@ private KeyPair readDecodedKeyPair(final PlainBuffer keyBuffer) throws IOExcepti |
135 | 155 |
|
136 | 156 | int nrKeys = keyBuffer.readUInt32AsInt(); // int number of keys N; Should be 1 |
137 | 157 | if (nrKeys != 1) { |
138 | | - throw new IOException("We don't support having more than 1 key in the file (yet)."); |
| 158 | + final String message = String.format("OpenSSH Private Key number of keys not supported [%d]", nrKeys); |
| 159 | + throw new IOException(message); |
139 | 160 | } |
140 | 161 | PublicKey publicKey = pubKey; |
141 | 162 | if (publicKey == null) { |
142 | 163 | publicKey = readPublicKey(new PlainBuffer(keyBuffer.readBytes())); |
143 | | - } |
144 | | - else { |
| 164 | + } else { |
145 | 165 | keyBuffer.readBytes(); |
146 | 166 | } |
147 | | - PlainBuffer privateKeyBuffer = new PlainBuffer(keyBuffer.readBytes()); // string (possibly) encrypted, padded list of private keys |
148 | | - if ("none".equals(cipherName)) { |
149 | | - logger.debug("Reading unencrypted keypair"); |
| 167 | + |
| 168 | + final byte[] privateKeyEncoded = keyBuffer.readBytes(); |
| 169 | + final PlainBuffer privateKeyBuffer = new PlainBuffer(privateKeyEncoded); |
| 170 | + |
| 171 | + if (NONE_CIPHER.equals(cipherName)) { |
150 | 172 | return readUnencrypted(privateKeyBuffer, publicKey); |
151 | 173 | } else { |
152 | | - logger.info("Keypair is encrypted with: {}, {}, {}", cipherName, kdfName, Arrays.toString(kdfOptions)); |
| 174 | + final byte[] encryptedPrivateKey = readEncryptedPrivateKey(privateKeyEncoded, keyBuffer); |
153 | 175 | while (true) { |
154 | | - PlainBuffer decryptionBuffer = new PlainBuffer(privateKeyBuffer); |
155 | | - PlainBuffer decrypted = decryptBuffer(decryptionBuffer, cipherName, kdfName, kdfOptions); |
| 176 | + final byte[] encrypted = encryptedPrivateKey.clone(); |
156 | 177 | try { |
| 178 | + final PlainBuffer decrypted = decryptPrivateKey(encrypted, privateKeyEncoded.length, cipherName, kdfName, kdfOptions); |
157 | 179 | return readUnencrypted(decrypted, publicKey); |
158 | 180 | } catch (KeyDecryptionFailedException e) { |
159 | 181 | if (pwdf == null || !pwdf.shouldRetry(resource)) |
160 | 182 | throw e; |
161 | 183 | } |
162 | 184 | } |
163 | | -// throw new IOException("Cannot read encrypted keypair with " + cipherName + " yet."); |
164 | 185 | } |
165 | 186 | } |
166 | 187 |
|
167 | | - private PlainBuffer decryptBuffer(PlainBuffer privateKeyBuffer, String cipherName, String kdfName, byte[] kdfOptions) throws IOException { |
168 | | - Cipher cipher = createCipher(cipherName); |
169 | | - initializeCipher(kdfName, kdfOptions, cipher); |
170 | | - byte[] array = privateKeyBuffer.array(); |
171 | | - cipher.update(array, 0, privateKeyBuffer.available()); |
172 | | - return new PlainBuffer(array); |
| 188 | + private byte[] readEncryptedPrivateKey(final byte[] privateKeyEncoded, final PlainBuffer inputBuffer) throws Buffer.BufferException { |
| 189 | + final byte[] encryptedPrivateKey; |
| 190 | + |
| 191 | + final int bufferRemaining = inputBuffer.available(); |
| 192 | + if (bufferRemaining == 0) { |
| 193 | + encryptedPrivateKey = privateKeyEncoded; |
| 194 | + } else { |
| 195 | + // Read Authentication Tag for AES-GCM |
| 196 | + final byte[] authenticationTag = new byte[bufferRemaining]; |
| 197 | + inputBuffer.readRawBytes(authenticationTag); |
| 198 | + |
| 199 | + final int encryptedBufferLength = privateKeyEncoded.length + authenticationTag.length; |
| 200 | + final PlainBuffer encryptedBuffer = new PlainBuffer(encryptedBufferLength); |
| 201 | + encryptedBuffer.putRawBytes(privateKeyEncoded); |
| 202 | + encryptedBuffer.putRawBytes(authenticationTag); |
| 203 | + |
| 204 | + encryptedPrivateKey = new byte[encryptedBufferLength]; |
| 205 | + encryptedBuffer.readRawBytes(encryptedPrivateKey); |
| 206 | + } |
| 207 | + |
| 208 | + return encryptedPrivateKey; |
173 | 209 | } |
174 | 210 |
|
175 | | - private void initializeCipher(String kdfName, byte[] kdfOptions, Cipher cipher) throws Buffer.BufferException { |
| 211 | + private PlainBuffer decryptPrivateKey(final byte[] privateKey, final int privateKeyLength, final String cipherName, final String kdfName, final byte[] kdfOptions) throws IOException { |
| 212 | + try { |
| 213 | + final Cipher cipher = createCipher(cipherName); |
| 214 | + initializeCipher(kdfName, kdfOptions, cipher); |
| 215 | + cipher.update(privateKey, 0, privateKeyLength); |
| 216 | + } catch (final SSHRuntimeException e) { |
| 217 | + final String message = String.format("OpenSSH Private Key decryption failed with cipher [%s]", cipherName); |
| 218 | + throw new KeyDecryptionFailedException(new EncryptionException(message, e)); |
| 219 | + } |
| 220 | + final PlainBuffer decryptedPrivateKey = new PlainBuffer(privateKeyLength); |
| 221 | + decryptedPrivateKey.putRawBytes(privateKey, 0, privateKeyLength); |
| 222 | + return decryptedPrivateKey; |
| 223 | + } |
| 224 | + |
| 225 | + private void initializeCipher(final String kdfName, final byte[] kdfOptions, final Cipher cipher) throws Buffer.BufferException { |
176 | 226 | if (kdfName.equals(BCRYPT)) { |
177 | | - PlainBuffer opts = new PlainBuffer(kdfOptions); |
| 227 | + final PlainBuffer bufferedOptions = new PlainBuffer(kdfOptions); |
178 | 228 | byte[] passphrase = new byte[0]; |
179 | 229 | if (pwdf != null) { |
180 | | - CharBuffer charBuffer = CharBuffer.wrap(pwdf.reqPassword(null)); |
181 | | - ByteBuffer byteBuffer = Charset.forName("UTF-8").encode(charBuffer); |
| 230 | + final CharBuffer charBuffer = CharBuffer.wrap(pwdf.reqPassword(null)); |
| 231 | + final ByteBuffer byteBuffer = StandardCharsets.UTF_8.encode(charBuffer); |
182 | 232 | passphrase = Arrays.copyOfRange(byteBuffer.array(), byteBuffer.position(), byteBuffer.limit()); |
183 | 233 | Arrays.fill(charBuffer.array(), '\u0000'); |
184 | 234 | Arrays.fill(byteBuffer.array(), (byte) 0); |
185 | 235 | } |
186 | | - byte[] keyiv = new byte[cipher.getIVSize()+ cipher.getBlockSize()]; |
187 | | - new BCrypt().pbkdf(passphrase, opts.readBytes(), opts.readUInt32AsInt(), keyiv); |
| 236 | + |
| 237 | + final int ivSize = cipher.getIVSize(); |
| 238 | + final int blockSize = cipher.getBlockSize(); |
| 239 | + final int parameterSize = ivSize + blockSize; |
| 240 | + final byte[] keyIvParameters = new byte[parameterSize]; |
| 241 | + |
| 242 | + final byte[] salt = bufferedOptions.readBytes(); |
| 243 | + final int iterations = bufferedOptions.readUInt32AsInt(); |
| 244 | + new BCrypt().pbkdf(passphrase, salt, iterations, keyIvParameters); |
188 | 245 | Arrays.fill(passphrase, (byte) 0); |
189 | | - byte[] key = Arrays.copyOfRange(keyiv, 0, cipher.getBlockSize()); |
190 | | - byte[] iv = Arrays.copyOfRange(keyiv, cipher.getBlockSize(), cipher.getIVSize() + cipher.getBlockSize()); |
| 246 | + |
| 247 | + final byte[] key = Arrays.copyOfRange(keyIvParameters, 0, blockSize); |
| 248 | + final byte[] iv = Arrays.copyOfRange(keyIvParameters, blockSize, parameterSize); |
| 249 | + |
191 | 250 | cipher.init(Cipher.Mode.Decrypt, key, iv); |
192 | 251 | } else { |
193 | | - throw new IllegalStateException("No support for KDF '" + kdfName + "'."); |
| 252 | + final String message = String.format("OpenSSH Private Key encryption KDF not supported [%s]", kdfName); |
| 253 | + throw new IllegalStateException(message); |
194 | 254 | } |
195 | 255 | } |
196 | 256 |
|
197 | | - private Cipher createCipher(String cipherName) { |
198 | | - if (cipherName.equals(BlockCiphers.AES256CTR().getName())) { |
199 | | - return BlockCiphers.AES256CTR().create(); |
200 | | - } else if (cipherName.equals(BlockCiphers.AES256CBC().getName())) { |
201 | | - return BlockCiphers.AES256CBC().create(); |
202 | | - } else if (cipherName.equals(BlockCiphers.AES128CBC().getName())) { |
203 | | - return BlockCiphers.AES128CBC().create(); |
| 257 | + private Cipher createCipher(final String cipherName) { |
| 258 | + final Cipher cipher; |
| 259 | + |
| 260 | + if (SUPPORTED_CIPHERS.containsKey(cipherName)) { |
| 261 | + final Factory.Named<Cipher> cipherFactory = SUPPORTED_CIPHERS.get(cipherName); |
| 262 | + cipher = cipherFactory.create(); |
| 263 | + } else { |
| 264 | + final String message = String.format("OpenSSH Key encryption cipher not supported [%s]", cipherName); |
| 265 | + throw new IllegalStateException(message); |
204 | 266 | } |
205 | | - throw new IllegalStateException("Cipher '" + cipherName + "' not currently implemented for openssh-key-v1 format"); |
| 267 | + |
| 268 | + return cipher; |
206 | 269 | } |
207 | 270 |
|
208 | 271 | private PublicKey readPublicKey(final PlainBuffer plainBuffer) throws Buffer.BufferException, GeneralSecurityException { |
|
0 commit comments