blob: 2bfc9704c4e70298bb521c2d45dbbc554bade173 [file] [log] [blame]
/*
* 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;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.security.GeneralSecurityException;
import java.security.MessageDigest;
import java.security.SecureRandom;
import java.security.spec.InvalidKeySpecException;
import java.util.Arrays;
import java.util.Objects;
import java.util.Random;
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import org.apache.sshd.common.digest.BuiltinDigests;
import org.apache.sshd.common.util.GenericUtils;
import org.apache.sshd.common.util.ValidateUtils;
import org.apache.sshd.common.util.buffer.BufferUtils;
import org.apache.sshd.common.util.security.SecurityUtils;
/**
* @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
*/
public abstract class AbstractPrivateKeyObfuscator implements PrivateKeyObfuscator {
private final String algName;
protected AbstractPrivateKeyObfuscator(String name) {
algName = ValidateUtils.checkNotNullAndNotEmpty(name, "No name specified");
}
@Override
public final String getCipherName() {
return algName;
}
@Override
public byte[] generateInitializationVector(PrivateKeyEncryptionContext encContext)
throws GeneralSecurityException {
int ivSize = resolveInitializationVectorLength(encContext);
byte[] initVector = new byte[ivSize];
Random randomizer = new SecureRandom(); // TODO consider using some pre-created singleton instance
randomizer.nextBytes(initVector);
return initVector;
}
@Override
public <A extends Appendable> A appendPrivateKeyEncryptionContext(
A sb, PrivateKeyEncryptionContext encContext)
throws IOException {
if (encContext == null) {
return sb;
}
sb.append("DEK-Info: ").append(encContext.getCipherName())
.append('-').append(encContext.getCipherType())
.append('-').append(encContext.getCipherMode());
byte[] initVector = encContext.getInitVector();
Objects.requireNonNull(initVector, "No encryption init vector");
ValidateUtils.checkTrue(initVector.length > 0, "Empty encryption init vector");
BufferUtils.appendHex(sb.append(','), BufferUtils.EMPTY_HEX_SEPARATOR, initVector);
sb.append(System.lineSeparator());
return sb;
}
protected abstract int resolveInitializationVectorLength(PrivateKeyEncryptionContext encContext)
throws GeneralSecurityException;
protected abstract int resolveKeyLength(PrivateKeyEncryptionContext encContext) throws GeneralSecurityException;
// see http://martin.kleppmann.com/2013/05/24/improving-security-of-ssh-private-keys.html
// see http://www.ict.griffith.edu.au/anthony/info/crypto/openssl.hints (Password to Encryption Key section)
// see http://openssl.6102.n7.nabble.com/DES-EDE3-CBC-technical-details-td24883.html
protected byte[] deriveEncryptionKey(PrivateKeyEncryptionContext encContext, int outputKeyLength)
throws IOException, GeneralSecurityException {
Objects.requireNonNull(encContext, "No encryption context");
ValidateUtils.checkNotNullAndNotEmpty(encContext.getCipherName(), "No cipher name");
ValidateUtils.checkNotNullAndNotEmpty(encContext.getCipherType(), "No cipher type");
ValidateUtils.checkNotNullAndNotEmpty(encContext.getCipherMode(), "No cipher mode");
byte[] initVector = Objects.requireNonNull(encContext.getInitVector(), "No encryption init vector");
ValidateUtils.checkTrue(initVector.length > 0, "Empty encryption init vector");
String password = ValidateUtils.checkNotNullAndNotEmpty(encContext.getPassword(), "No encryption password");
byte[] passBytes = password.getBytes(StandardCharsets.UTF_8);
byte[] prevHash = GenericUtils.EMPTY_BYTE_ARRAY;
try {
byte[] keyValue = new byte[outputKeyLength];
MessageDigest hash = SecurityUtils.getMessageDigest(BuiltinDigests.Constants.MD5);
for (int index = 0, remLen = keyValue.length; index < keyValue.length;) {
hash.reset(); // just making sure
hash.update(prevHash, 0, prevHash.length);
hash.update(passBytes, 0, passBytes.length);
hash.update(initVector, 0, Math.min(initVector.length, 8));
prevHash = hash.digest();
System.arraycopy(prevHash, 0, keyValue, index, Math.min(remLen, prevHash.length));
index += prevHash.length;
remLen -= prevHash.length;
}
return keyValue;
} finally {
password = null;
Arrays.fill(passBytes, (byte) 0); // clean up sensitive data a.s.a.p.
Arrays.fill(prevHash, (byte) 0); // clean up sensitive data a.s.a.p.
}
}
protected byte[] applyPrivateKeyCipher(
byte[] bytes, PrivateKeyEncryptionContext encContext, int numBits, byte[] keyValue, boolean encryptIt)
throws IOException, GeneralSecurityException {
Objects.requireNonNull(encContext, "No encryption context");
String cipherName = ValidateUtils.checkNotNullAndNotEmpty(encContext.getCipherName(), "No cipher name");
ValidateUtils.checkNotNullAndNotEmpty(encContext.getCipherType(), "No cipher type");
String cipherMode = ValidateUtils.checkNotNullAndNotEmpty(encContext.getCipherMode(), "No cipher mode");
Objects.requireNonNull(bytes, "No source data");
Objects.requireNonNull(keyValue, "No encryption key");
ValidateUtils.checkTrue(keyValue.length > 0, "Empty encryption key");
byte[] initVector = Objects.requireNonNull(encContext.getInitVector(), "No encryption init vector");
ValidateUtils.checkTrue(initVector.length > 0, "Empty encryption init vector");
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(
"applyPrivateKeyCipher(" + xform + ")[encrypt=" + encryptIt + "]"
+ " required key length (" + numBits + ")"
+ " exceeds max. available: " + maxAllowedBits);
}
SecretKeySpec skeySpec = new SecretKeySpec(keyValue, cipherName);
IvParameterSpec ivspec = new IvParameterSpec(initVector);
Cipher cipher = SecurityUtils.getCipher(xform);
int blockSize = cipher.getBlockSize();
int dataSize = bytes.length;
cipher.init(encryptIt ? Cipher.ENCRYPT_MODE : Cipher.DECRYPT_MODE, skeySpec, ivspec);
if (blockSize <= 0) {
return cipher.doFinal(bytes);
}
int remLen = dataSize % blockSize;
if (remLen <= 0) {
return cipher.doFinal(bytes);
}
int updateSize = dataSize - remLen;
byte[] lastBlock = new byte[blockSize];
// TODO for some reason, calling cipher.update followed by cipher.doFinal does not work
ByteArrayOutputStream baos = new ByteArrayOutputStream(dataSize);
try {
Arrays.fill(lastBlock, (byte) 10);
System.arraycopy(bytes, updateSize, lastBlock, 0, remLen);
try {
byte[] buf = cipher.update(bytes, 0, updateSize);
try {
baos.write(buf);
} finally {
Arrays.fill(buf, (byte) 0); // get rid of sensitive data a.s.a.p.
}
buf = cipher.doFinal(lastBlock);
try {
baos.write(buf);
} finally {
Arrays.fill(buf, (byte) 0); // get rid of sensitive data a.s.a.p.
}
} finally {
baos.close();
}
} finally {
Arrays.fill(lastBlock, (byte) 0);
}
return baos.toByteArray();
}
}