All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.apache.sshd.putty.PuttyKeyPairResourceParser Maven / Gradle / Ivy

The newest version!
/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements. See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership. The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License. You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied. See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

package org.apache.sshd.putty;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.security.GeneralSecurityException;
import java.security.KeyPair;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.spec.InvalidKeySpecException;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;

import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;

import org.apache.sshd.common.NamedResource;
import org.apache.sshd.common.config.keys.IdentityResourceLoader;
import org.apache.sshd.common.config.keys.loader.KeyPairResourceParser;
import org.apache.sshd.common.digest.BuiltinDigests;
import org.apache.sshd.common.util.GenericUtils;
import org.apache.sshd.common.util.MapEntryUtils;
import org.apache.sshd.common.util.ValidateUtils;
import org.apache.sshd.common.util.buffer.BufferUtils;
import org.apache.sshd.common.util.security.SecurityUtils;
import org.bouncycastle.crypto.generators.Argon2BytesGenerator;
import org.bouncycastle.crypto.params.Argon2Parameters;

//CHECKSTYLE:OFF
/**
 * Loads a {@link KeyPair} from PuTTY's ".ppk" file.
 * 

Note(s):

* * *

Sample PuTTY file format

*
 * PuTTY-User-Key-File-2: ssh-rsa
 * Encryption: none
 * Comment: rsa-key-20080514
 * Public-Lines: 4
 * AAAAB3NzaC1yc2EAAAABJQAAAIEAiPVUpONjGeVrwgRPOqy3Ym6kF/f8bltnmjA2
 * BMdAtaOpiD8A2ooqtLS5zWYuc0xkW0ogoKvORN+RF4JI+uNUlkxWxnzJM9JLpnvA
 * HrMoVFaQ0cgDMIHtE1Ob1cGAhlNInPCRnGNJpBNcJ/OJye3yt7WqHP4SPCCLb6nL
 * nmBUrLM=
 * Private-Lines: 8
 * AAAAgGtYgJzpktzyFjBIkSAmgeVdozVhgKmF6WsDMUID9HKwtU8cn83h6h7ug8qA
 * hUWcvVxO201/vViTjWVz9ALph3uMnpJiuQaaNYIGztGJBRsBwmQW9738pUXcsUXZ
 * 79KJP01oHn6Wkrgk26DIOsz04QOBI6C8RumBO4+F1WdfueM9AAAAQQDmA4hcK8Bx
 * nVtEpcF310mKD3nsbJqARdw5NV9kCxPnEsmy7Sy1L4Ob/nTIrynbc3MA9HQVJkUz
 * 7V0va5Pjm/T7AAAAQQCYbnG0UEekwk0LG1Hkxh1OrKMxCw2KWMN8ac3L0LVBg/Tk
 * 8EnB2oT45GGeJaw7KzdoOMFZz0iXLsVLNUjNn2mpAAAAQQCN6SEfWqiNzyc/w5n/
 * lFVDHExfVUJp0wXv+kzZzylnw4fs00lC3k4PZDSsb+jYCMesnfJjhDgkUA0XPyo8
 * Emdk
 * Private-MAC: 50c45751d18d74c00fca395deb7b7695e3ed6f77
 * 
* @param Generic public key type * @param Generic private key type * @author Apache MINA SSHD Project */ //CHECKSTYLE:ON public interface PuttyKeyPairResourceParser extends IdentityResourceLoader, KeyPairResourceParser { String KEY_FILE_HEADER_PREFIX = "PuTTY-User-Key-File-"; String PUBLIC_LINES_HEADER = "Public-Lines"; String PRIVATE_LINES_HEADER = "Private-Lines"; String PPK_FILE_SUFFIX = ".ppk"; List KNOWN_HEADERS = Collections.unmodifiableList( Arrays.asList( KEY_FILE_HEADER_PREFIX, PUBLIC_LINES_HEADER, PRIVATE_LINES_HEADER)); /** * Value (case insensitive) used to denote that private key is not encrypted */ String NO_PRIVATE_KEY_ENCRYPTION_VALUE = "none"; /** PUTTY key v3 MAC key length */ int FORMAT_3_MAC_KEY_LENGTH = 32; @Override default boolean canExtractKeyPairs(NamedResource resourceKey, List lines) throws IOException, GeneralSecurityException { if (GenericUtils.isEmpty(lines)) { return false; } for (String l : lines) { l = GenericUtils.trimToEmpty(l); for (String hdr : KNOWN_HEADERS) { if (l.startsWith(hdr)) { return true; } } } return false; } static byte[] decodePrivateKeyBytes( int formatVersion, byte[] prvBytes, String algName, int numBits, String algMode, String password, Map headers) throws GeneralSecurityException { Objects.requireNonNull(prvBytes, "No encrypted key bytes"); ValidateUtils.checkNotNullAndNotEmpty(algName, "No encryption algorithm", GenericUtils.EMPTY_OBJECT_ARRAY); ValidateUtils.checkTrue(numBits > 0, "Invalid encryption key size: %d", numBits); ValidateUtils.checkNotNullAndNotEmpty(algMode, "No encryption mode", GenericUtils.EMPTY_OBJECT_ARRAY); ValidateUtils.checkNotNullAndNotEmpty(password, "No encryption password", GenericUtils.EMPTY_OBJECT_ARRAY); if (!"AES".equalsIgnoreCase(algName)) { throw new NoSuchAlgorithmException("decodePrivateKeyBytes(" + algName + "-" + numBits + "-" + algMode + ") N/A"); } if ((numBits != 128) && (numBits != 192) && (numBits != 256)) { throw new InvalidKeySpecException("Requested key size (" + numBits + ") is not supported"); } byte[] initVector = new byte[16]; byte[] keyValue = new byte[numBits / Byte.SIZE]; decodeEncryptionKey(formatVersion, password, initVector, keyValue, headers); try { return decodePrivateKeyBytes(prvBytes, algName, algMode, numBits, initVector, keyValue); } finally { Arrays.fill(initVector, (byte) 0); // eliminate sensitive data a.s.a.p. Arrays.fill(keyValue, (byte) 0); // eliminate sensitive data a.s.a.p. } } static byte[] decodePrivateKeyBytes( byte[] encBytes, String cipherName, String cipherMode, int numBits, byte[] initVector, byte[] keyValue) throws GeneralSecurityException { String xform = cipherName + "/" + cipherMode + "/NoPadding"; int maxAllowedBits = Cipher.getMaxAllowedKeyLength(xform); // see http://www.javamex.com/tutorials/cryptography/unrestricted_policy_files.shtml if (numBits > maxAllowedBits) { throw new InvalidKeySpecException("decodePrivateKeyBytes(" + xform + ")" + " required key length (" + numBits + ") exceeds max. available: " + maxAllowedBits); } SecretKeySpec skeySpec = new SecretKeySpec(keyValue, cipherName); IvParameterSpec ivspec = new IvParameterSpec(initVector); Cipher cipher = SecurityUtils.getCipher(xform); cipher.init(Cipher.DECRYPT_MODE, skeySpec, ivspec); return cipher.doFinal(encBytes); } /** * Converts a pass-phrase into a key, by following the conventions that PuTTY uses. Used to decrypt the private key * when it's encrypted. * * @param formatVersion The file format version * @param passphrase The Password to be used as seed for the key - ignored if {@code null}/empty * @param iv Initialization vector to be populated if necessary * @param key Key to be populated * @param headers Any extra headers found in the PPK file that might be used for KDF * @throws GeneralSecurityException If cannot derive the key bytes from the password */ static void decodeEncryptionKey( int formatVersion, String passphrase, byte[] iv, byte[] key, Map headers) throws GeneralSecurityException { String keyDerivationType = getStringHeaderValue(headers, "Key-Derivation"); if (GenericUtils.isBlank(keyDerivationType)) { deriveFormat2EncryptionKey(passphrase, iv, key); } else if ("Argon2id".equalsIgnoreCase(keyDerivationType) || "Argon2i".equalsIgnoreCase(keyDerivationType) || "Argon2d".equalsIgnoreCase(keyDerivationType)) { deriveFormat3EncryptionKey(passphrase, keyDerivationType, iv, key, headers); } else { throw new NoSuchAlgorithmException("Unsupported KDF method: " + keyDerivationType); } } static void deriveFormat3EncryptionKey( String passphrase, String keyDerivationType, byte[] iv, byte[] key, Map headers) throws GeneralSecurityException { ValidateUtils.checkNotNullAndNotEmpty(headers, "Mising file headers for KDF purposes"); Objects.requireNonNull(passphrase, "No passphrase provded"); int parallelism = getIntegerHeaderValue(headers, "Argon2-Parallelism"); int iterations = getIntegerHeaderValue(headers, "Argon2-Passes"); int memory = getIntegerHeaderValue(headers, "Argon2-Memory"); byte[] salt = ValidateUtils.checkNotNullAndNotEmpty( getHexArrayHeaderValue(headers, "Argon2-Salt"), "No Argon2 salt value provided"); byte[] hashValue = new byte[key.length + iv.length + FORMAT_3_MAC_KEY_LENGTH]; byte[] passBytes = passphrase.getBytes(StandardCharsets.UTF_8); try { Argon2Parameters.Builder builder; if ("Argon2id".equalsIgnoreCase(keyDerivationType)) { builder = new Argon2Parameters.Builder(Argon2Parameters.ARGON2_id); } else if ("Argon2i".equalsIgnoreCase(keyDerivationType)) { builder = new Argon2Parameters.Builder(Argon2Parameters.ARGON2_i); } else if ("Argon2d".equalsIgnoreCase(keyDerivationType)) { builder = new Argon2Parameters.Builder(Argon2Parameters.ARGON2_i); } else { throw new NoSuchAlgorithmException("Unsupported key derivation type: " + keyDerivationType); } Argon2Parameters params = builder .withSalt(salt) .withParallelism(parallelism) .withMemoryAsKB(memory) .withIterations(iterations) .build(); Argon2BytesGenerator generator = new Argon2BytesGenerator(); generator.init(params); generator.generateBytes(passBytes, hashValue); } finally { Arrays.fill(passBytes, (byte) 0); // eliminate sensitive data a.s.a.p. } try { System.arraycopy(hashValue, 0, key, 0, key.length); System.arraycopy(hashValue, key.length, iv, 0, iv.length); } finally { Arrays.fill(hashValue, (byte) 0); // eliminate sensitive data a.s.a.p. } } static String getStringHeaderValue(Map headers, String key) { return MapEntryUtils.isEmpty(headers) ? null : headers.get(key); } static byte[] getHexArrayHeaderValue(Map headers, String key) { String value = getStringHeaderValue(headers, key); return BufferUtils.decodeHex(BufferUtils.EMPTY_HEX_SEPARATOR, value); } static int getIntegerHeaderValue(Map headers, String key) { String value = ValidateUtils.checkNotNullAndNotEmpty(getStringHeaderValue(headers, key), "Missing %s header value", key); return Integer.parseInt(value); } /** * Uses the "legacy" KDF via SHA-1 * * @param passphrase The Password to be used as seed for the key - ignored if {@code null}/empty * @param iv Initialization vector to be populated if necessary * @param key Key to be populated * @throws GeneralSecurityException If cannot retrieve SHA-1 digest * @see * How does Putty derive the encryption key in its .ppk format ? */ static void deriveFormat2EncryptionKey(String passphrase, byte[] iv, byte[] key) throws GeneralSecurityException { Objects.requireNonNull(passphrase, "No passphrase provded"); byte[] passBytes = passphrase.getBytes(StandardCharsets.UTF_8); try { MessageDigest hash = SecurityUtils.getMessageDigest(BuiltinDigests.sha1.getAlgorithm()); byte[] stateValue = { 0, 0, 0, 0 }; try { for (int i = 0, remLen = key.length; remLen > 0; i++) { hash.reset(); // just making sure stateValue[3] = (byte) i; hash.update(stateValue); hash.update(passBytes); byte[] digest = hash.digest(); try { System.arraycopy(digest, 0, key, i * 20, Math.min(20, remLen)); } finally { Arrays.fill(digest, (byte) 0); // eliminate sensitive data a.s.a.p. } remLen -= 20; } } finally { Arrays.fill(stateValue, (byte) 0); // eliminate sensitive data a.s.a.p. } Arrays.fill(iv, (byte) 0); } finally { Arrays.fill(passBytes, (byte) 0); // eliminate sensitive data a.s.a.p. } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy