/* * 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.common.config.keys.loader.putty; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.io.StreamCorruptedException; import java.security.GeneralSecurityException; import java.security.KeyPair; import java.util.Base64; import java.util.Base64.Decoder; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Objects; import org.apache.sshd.common.config.keys.FilePasswordProvider; import org.apache.sshd.common.config.keys.loader.KeyPairResourceParser; import org.apache.sshd.common.util.GenericUtils; import org.apache.sshd.common.util.ValidateUtils; import org.apache.sshd.common.util.logging.AbstractLoggingBean; /** * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a> */ public abstract class AbstractPuttyKeyDecoder extends AbstractLoggingBean implements PuttyKeyPairResourceParser { public static final String ENCRYPTION_HEADER = "Encryption"; private final String keyType; protected AbstractPuttyKeyDecoder(String keyType) { this.keyType = keyType; } @Override public String getKeyType() { return keyType; } @Override public boolean canExtractKeyPairs(String resourceKey, List<String> lines) throws IOException, GeneralSecurityException { if (!PuttyKeyPairResourceParser.super.canExtractKeyPairs(resourceKey, lines)) { return false; } for (String l : lines) { l = GenericUtils.trimToEmpty(l); if (!l.startsWith(KEY_FILE_HEADER_PREFIX)) { continue; } int pos = l.indexOf(':'); if ((pos <= 0) || (pos >= (l.length() - 1))) { return false; } String typeValue = l.substring(pos + 1).trim(); return Objects.equals(getKeyType(), typeValue); } return false; } @Override public Collection<KeyPair> loadKeyPairs( String resourceKey, FilePasswordProvider passwordProvider, List<String> lines) throws IOException, GeneralSecurityException { List<String> pubLines = Collections.emptyList(); List<String> prvLines = Collections.emptyList(); String prvEncryption = null; for (int index = 0, numLines = lines.size(); index < numLines; index++) { String l = lines.get(index); l = GenericUtils.trimToEmpty(l); int pos = l.indexOf(':'); if ((pos <= 0) || (pos >= (l.length() - 1))) { continue; } String hdrName = l.substring(0, pos).trim(); String hdrValue = l.substring(pos + 1).trim(); switch (hdrName) { case ENCRYPTION_HEADER: if (prvEncryption != null) { throw new StreamCorruptedException("Duplicate " + hdrName + " in" + resourceKey); } prvEncryption = hdrValue; break; case PUBLIC_LINES_HEADER: pubLines = extractDataLines(resourceKey, lines, index + 1, hdrName, hdrValue, pubLines); index += pubLines.size(); break; case PRIVATE_LINES_HEADER: prvLines = extractDataLines(resourceKey, lines, index + 1, hdrName, hdrValue, prvLines); index += prvLines.size(); break; default: // ignored } } return loadKeyPairs(resourceKey, pubLines, prvLines, prvEncryption, passwordProvider); } public static List<String> extractDataLines( String resourceKey, List<String> lines, int startIndex, String hdrName, String hdrValue, List<String> curLines) throws IOException { if (GenericUtils.size(curLines) > 0) { throw new StreamCorruptedException("Duplicate " + hdrName + " in " + resourceKey); } int numLines; try { numLines = Integer.parseInt(hdrValue); } catch (NumberFormatException e) { throw new StreamCorruptedException("Bad " + hdrName + " value (" + hdrValue + ") in " + resourceKey); } int endIndex = startIndex + numLines; int totalLines = lines.size(); if (endIndex > totalLines) { throw new StreamCorruptedException("Excessive " + hdrName + " value (" + hdrValue + ") in " + resourceKey); } return lines.subList(startIndex, endIndex); } public Collection<KeyPair> loadKeyPairs( String resourceKey, List<String> pubLines, List<String> prvLines, String prvEncryption, FilePasswordProvider passwordProvider) throws IOException, GeneralSecurityException { return loadKeyPairs(resourceKey, KeyPairResourceParser.joinDataLines(pubLines), KeyPairResourceParser.joinDataLines(prvLines), prvEncryption, passwordProvider); } public Collection<KeyPair> loadKeyPairs( String resourceKey, String pubData, String prvData, String prvEncryption, FilePasswordProvider passwordProvider) throws IOException, GeneralSecurityException { Decoder b64Decoder = Base64.getDecoder(); byte[] pubBytes = b64Decoder.decode(pubData); byte[] prvBytes = b64Decoder.decode(prvData); String password = null; if ((GenericUtils.length(prvEncryption) > 0) && (!NO_PRIVATE_KEY_ENCRYPTION_VALUE.equalsIgnoreCase(prvEncryption))) { password = passwordProvider.getPassword(resourceKey); } if (GenericUtils.isEmpty(prvEncryption) || NO_PRIVATE_KEY_ENCRYPTION_VALUE.equalsIgnoreCase(prvEncryption) || GenericUtils.isEmpty(password)) { return loadKeyPairs(resourceKey, pubBytes, prvBytes); } // format is "<cipher><bits>-<mode>" - e.g., "aes256-cbc" int pos = prvEncryption.indexOf('-'); if (pos <= 0) { throw new StreamCorruptedException("Missing private key encryption mode in " + prvEncryption); } String mode = prvEncryption.substring(pos + 1).toUpperCase(); String algName = null; int numBits = 0; for (int index = 0; index < pos; index++) { char ch = prvEncryption.charAt(index); if ((ch >= '0') && (ch <= '9')) { algName = prvEncryption.substring(0, index).toUpperCase(); numBits = Integer.parseInt(prvEncryption.substring(index, pos)); break; } } if (GenericUtils.isEmpty(algName) || (numBits <= 0)) { throw new StreamCorruptedException("Missing private key encryption algorithm details in " + prvEncryption); } prvBytes = PuttyKeyPairResourceParser.decodePrivateKeyBytes(prvBytes, algName, numBits, mode, password); return loadKeyPairs(resourceKey, pubBytes, prvBytes); } public Collection<KeyPair> loadKeyPairs(String resourceKey, byte[] pubData, byte[] prvData) throws IOException, GeneralSecurityException { ValidateUtils.checkNotNullAndNotEmpty(pubData, "No public key data in %s", resourceKey); ValidateUtils.checkNotNullAndNotEmpty(prvData, "No private key data in %s", resourceKey); try (InputStream pubStream = new ByteArrayInputStream(pubData); InputStream prvStream = new ByteArrayInputStream(prvData)) { return loadKeyPairs(resourceKey, pubStream, prvStream); } } public Collection<KeyPair> loadKeyPairs(String resourceKey, InputStream pubData, InputStream prvData) throws IOException, GeneralSecurityException { try (PuttyKeyReader pubReader = new PuttyKeyReader(ValidateUtils.checkNotNull(pubData, "No public key data in %s", resourceKey)); PuttyKeyReader prvReader = new PuttyKeyReader(ValidateUtils.checkNotNull(prvData, "No private key data in %s", resourceKey))) { return loadKeyPairs(resourceKey, pubReader, prvReader); } } public abstract Collection<KeyPair> loadKeyPairs(String resourceKey, PuttyKeyReader pubReader, PuttyKeyReader prvReader) throws IOException, GeneralSecurityException; }