/*
* 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;
}