/*
* Copyright 2012-2013 Mathias Herberts
*
* Licensed 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 com.geoxp.oss;
import java.io.UnsupportedEncodingException;
import java.math.BigInteger;
import java.nio.ByteBuffer;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import org.bouncycastle.crypto.AsymmetricCipherKeyPair;
import org.bouncycastle.crypto.generators.RSAKeyPairGenerator;
import org.bouncycastle.crypto.params.RSAKeyGenerationParameters;
import org.bouncycastle.crypto.params.RSAKeyParameters;
import org.bouncycastle.util.encoders.Hex;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.geoxp.oss.client.OSSClient;
import com.geoxp.oss.servlet.GetSecretServlet;
public class OSS {
private static final Logger LOGGER = LoggerFactory.getLogger(OSS.class);
/**
* Default strength for temporary RSA keys
*/
public static final int DEFAULT_RSA_STRENGTH = 4096;
/**
* Size of nonce to append to secrets prior to wrapping them. This is so
* two identical secrets do not appear as identical secret files after wrapping.
*/
public static final int NONCE_BYTES = 8;
/**
* Name of servlet context init parameter containing the maximum secret size in bytes
*/
public static final String CONTEXT_PARAM_OSS_MAX_SECRET_SIZE = "oss.max.secret.size";
/**
* Name of servlet context init parameter containing the token TTL in ms
*/
public static final String CONTEXT_PARAM_OSS_TOKEN_TTL = "oss.token.ttl";
/**
* Name of servlet context init parameter containing the list of SSH keys that can call gensecret
*/
public static final String CONTEXT_PARAM_OSS_GENSECRET_SSHKEYS = "oss.gensecret.sshkeys";
/**
* Name of servlet context init parameter containing the list of SSH keys that can call putsecret
*/
public static final String CONTEXT_PARAM_OSS_PUTSECRET_SSHKEYS = "oss.putsecret.sshkeys";
/**
* Name of servlet context init parameter containing the list of SSH keys that can call init
*/
public static final String CONTEXT_PARAM_OSS_INIT_SSHKEYS = "oss.init.sshkeys";
/**
* Name of servlet context init parameter containing the list of SSH keys that can access ACLs
* If this parameter is set, secure ACLs are used.
*/
public static final String CONTEXT_PARAM_OSS_ACL_SSHKEYS = "oss.acl.sshkeys";
/**
* Directory where secrets are stored
*/
public static final String CONTEXT_PARAM_OSS_KEYSTORE_DIR = "oss.keystore.dir";
/**
* This is the master secret used by the instance of Open Secret Server to protect
* the secrets it manages.
*/
private static ByteBuffer MASTER_SECRET = null;
/**
* Public key part of a session RSA key pair.
* This is a key pair generated at startup time and used
* to protect data exchanges with the OSS.
*/
private static RSAPublicKey SESSION_RSA_PUBLIC;
/**
* Private key part of the session RSA key pair
*/
private static RSAPrivateKey SESSION_RSA_PRIVATE;
/**
* Maximum size of a secret
*/
private static int MAX_SECRET_SIZE = 32;
/**
* Maximum allowed age of received tokens, in ms
*/
private static long MAX_TOKEN_AGE = 5000L;
/**
* Set of SSH key fingerprints which can generate new secrets
*/
private static final Set<String> GENSECRET_AUTHORIZED_SSHKEYS = new HashSet<String>();
/**
* Set of SSH key fingerprints which can init the Open Secret Server
*/
private static final Set<String> INIT_AUTHORIZED_SSHKEYS = new HashSet<String>();
/**
* Set of SSH key fingerprints which can store secrets
*/
private static final Set<String> PUTSECRET_AUTHORIZED_SSHKEYS = new HashSet<String>();
/**
* Set of SSH key fingerprints which can access ACLs
*/
private static final Set<String> ACL_AUTHORIZED_SSHKEYS = new HashSet<String>();
private static KeyStore KEYSTORE;
private static final Set<byte[]> initSecrets = new HashSet<byte[]>();
static {
//
// Generate session RSA key pair
//
RSAKeyPairGenerator gen = new RSAKeyPairGenerator();
// For explanation of 'certainty', refer to http://bouncy-castle.1462172.n4.nabble.com/Questions-about-RSAKeyGenerationParameters-td1463186.html
RSAKeyGenerationParameters params = new RSAKeyGenerationParameters(new BigInteger("65537"), CryptoHelper.getSecureRandom(), DEFAULT_RSA_STRENGTH, 64);
gen.init(params);
final AsymmetricCipherKeyPair keypair = gen.generateKeyPair();
SESSION_RSA_PRIVATE = new RSAPrivateKey() {
public BigInteger getModulus() { return ((RSAKeyParameters) keypair.getPrivate()).getModulus(); }
public String getFormat() { return "PKCS#8"; }
public byte[] getEncoded() { return null; }
public String getAlgorithm() { return "RSA"; }
public BigInteger getPrivateExponent() { return ((RSAKeyParameters) keypair.getPrivate()).getExponent(); }
};
SESSION_RSA_PUBLIC = new RSAPublicKey() {
public BigInteger getModulus() { return ((RSAKeyParameters) keypair.getPublic()).getModulus(); }
public String getFormat() { return "PKCS#8"; }
public byte[] getEncoded() { return null; }
public String getAlgorithm() { return "RSA"; }
public BigInteger getPublicExponent() { return ((RSAKeyParameters) keypair.getPublic()).getExponent(); }
};
//
// Output log line with public key fingerprint
//
LOGGER.info("Use '-D" + OSSClient.OSS_RSA + "=" + SESSION_RSA_PUBLIC.getModulus() + ":" + SESSION_RSA_PUBLIC.getPublicExponent() + "' to ensure you're talking to this OSS instance when calling Init/PutSecret/ChangeACL.");
}
public static byte[] getMasterSecret() {
byte[] k = new byte[MASTER_SECRET.limit()];
ByteBuffer bb = MASTER_SECRET.duplicate();
bb.position(0);
bb.get(k);
return k;
}
public static RSAPublicKey getSessionRSAPublicKey() {
return SESSION_RSA_PUBLIC;
}
public static RSAPrivateKey getSessionRSAPrivateKey() {
return SESSION_RSA_PRIVATE;
}
public static long getMaxTokenAge() {
return MAX_TOKEN_AGE;
}
public static int getMaxSecretSize() {
return MAX_SECRET_SIZE;
}
public static void setMaxSecretSize(String size) {
MAX_SECRET_SIZE = Integer.valueOf(size);
}
public static void setMaxTokenAge(String ttl) {
if (null == ttl) {
return;
}
MAX_TOKEN_AGE = Long.valueOf(ttl);
}
public static void setGenSecretSSHKeys(String keylist) {
setSSHKeys(GENSECRET_AUTHORIZED_SSHKEYS, keylist);
}
public static void setInitSSHKeys(String keylist) {
setSSHKeys(INIT_AUTHORIZED_SSHKEYS, keylist);
}
public static void setPutSecretSSHKeys(String keylist) {
setSSHKeys(PUTSECRET_AUTHORIZED_SSHKEYS, keylist);
}
public static void setACLSSHKeys(String keylist) {
setSSHKeys(ACL_AUTHORIZED_SSHKEYS, keylist);
}
private static void setSSHKeys(Set<String> keyset, String keylist) {
if (null == keylist) {
return;
}
String[] keys = keylist.split(",");
keyset.clear();
for (String key: keys) {
key = key.toLowerCase().replaceAll("[^0-9a-f]", "");
if (32 == key.length()) {
keyset.add(key);
}
}
}
public static boolean checkGenSecretSSHKey(byte[] keyblob) {
return checkSSHKey(GENSECRET_AUTHORIZED_SSHKEYS, keyblob);
}
public static boolean checkInitSSHKey(byte[] keyblob) {
return checkSSHKey(INIT_AUTHORIZED_SSHKEYS, keyblob);
}
public static boolean checkPutSecretSSHKey(byte[] keyblob) {
return checkSSHKey(PUTSECRET_AUTHORIZED_SSHKEYS, keyblob);
}
public static boolean checkACLSSHKey(byte[] keyblob) {
return checkSSHKey(ACL_AUTHORIZED_SSHKEYS, keyblob);
}
public static boolean hasSecureACLs() {
return !ACL_AUTHORIZED_SSHKEYS.isEmpty();
}
private static boolean checkSSHKey(Set<String> keys, byte[] keyblob) {
byte[] keyfpr = CryptoHelper.sshKeyBlobFingerprint(keyblob);
String fpr = null;
try {
fpr = new String(Hex.encode(keyfpr), "UTF-8");
} catch (UnsupportedEncodingException uee) {
}
return keys.contains(fpr);
}
public static void setKeyStoreDirectory(String dir) {
if (null == dir) {
return;
}
KEYSTORE = new DirectoryHierarchyKeyStore(dir);
}
public static KeyStore getKeyStore() {
return KEYSTORE;
}
public static class OSSToken {
private final long ts;
private final byte[] secret;
private final byte[] keyblob;
public OSSToken(long ts, byte[] secret, byte[] keyblob) {
this.ts = ts;
this.secret = secret;
this.keyblob = keyblob;
}
public long getTs() {
return ts;
}
public byte[] getKeyblob() {
return keyblob;
}
public byte[] getSecret() {
return secret;
}
}
public static OSSToken checkToken(byte[] token) throws OSSException {
//
// Extract token parts
//
int offset = 0;
byte[] tsdata = CryptoHelper.decodeNetworkString(token, offset);
offset += 4 + tsdata.length;
byte[] secret = CryptoHelper.decodeNetworkString(token, offset);
offset += 4 + secret.length;
byte[] keyblob = CryptoHelper.decodeNetworkString(token, offset);
offset += 4 + keyblob.length;
int signedlen = offset;
byte[] sigblob = CryptoHelper.decodeNetworkString(token, offset);
//
// Check token timestamp
//
long ts = 0;
for (int i = 0; i < 8; i++) {
ts <<= 8;
ts |= (tsdata[i] & 0xffL);
}
if (System.currentTimeMillis() - ts > OSS.getMaxTokenAge()) {
throw new OSSException("OSS Token has expired.");
}
//
// Verify signature
//
if (!CryptoHelper.sshSignatureBlobVerify(token, 0, signedlen, sigblob, CryptoHelper.sshKeyBlobToPublicKey(keyblob))) {
throw new OSSException("Invalid signature of OSS Token.");
}
return new OSSToken(ts, secret, keyblob);
}
/**
* Add an initialization secret
*
* @param secret
*/
public static void init(byte[] secret) throws OSSException {
//
// Check if the secret was wrapped with the master wrapping key.
// If so this means the secret is the wrapped master secret.
//
byte[] clearsecret = CryptoHelper.unwrapAES(MasterSecretGenerator.getMasterSecretWrappingKey(), secret);
if (null != clearsecret) {
MASTER_SECRET = ByteBuffer.allocateDirect(clearsecret.length);
MASTER_SECRET.put(clearsecret);
Arrays.fill(clearsecret, (byte) 0);
initSecrets.clear();
return;
}
//
// Otherwise, attempt to recover secret split with Shamir Scheme
//
initSecrets.add(secret);
byte[] mastersecret = CryptoHelper.SSSSRecover(initSecrets);
if (null == mastersecret) {
throw new OSSException("Unable to recover master secret.");
}
//
// Attempt to unwrap master secret
//
byte[] sec = CryptoHelper.unwrapAES(MasterSecretGenerator.getMasterSecretWrappingKey(), mastersecret);
if (null != sec) {
MASTER_SECRET = ByteBuffer.allocateDirect(sec.length);
MASTER_SECRET.put(sec);
Arrays.fill(sec, (byte) 0);
initSecrets.clear();
return;
}
}
public static boolean isInitialized() {
return null != MASTER_SECRET;
}
}