/*
* Copyright (c) 2013 EMC Corporation
* All Rights Reserved
*/
package com.emc.storageos.security.authentication;
import java.net.URI;
import javax.crypto.SecretKey;
import org.apache.commons.codec.binary.Base64;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import com.emc.storageos.coordinator.client.service.CoordinatorClient;
import com.emc.storageos.db.client.URIUtil;
import com.emc.storageos.db.client.model.GenericSerializer;
import com.emc.storageos.db.client.model.SerializationIndex;
import com.emc.storageos.db.common.VdcUtil;
import com.emc.storageos.geomodel.TokenResponse;
import com.emc.storageos.security.SignatureHelper;
import com.emc.storageos.security.TokenEncodingDisabler;
import com.emc.storageos.security.authentication.TokenKeyGenerator.KeyIdKeyPair;
import com.emc.storageos.security.authentication.TokenKeyGenerator.TokenKeysBundle;
import static com.emc.storageos.security.authentication.TokenKeyGenerator.TOKEN_SIGNING_ALGO;
import com.emc.storageos.security.exceptions.SecurityException;
import com.emc.storageos.security.geo.GeoClientCacheManager;
import com.emc.storageos.security.geo.InterVDCTokenCacheHelper;
import com.emc.storageos.security.geo.TokenResponseBuilder;
import com.emc.storageos.security.geo.TokenResponseBuilder.TokenResponseArtifacts;
import com.emc.storageos.svcs.errorhandling.resources.APIException;
/*
* Base64 implementation of the TokenEncoder. This class encodes and decodes TokenOnWire objects.
* Additionally it signs them during encode(), and verifies their signature during decode().
*/
public class Base64TokenEncoder implements TokenEncoder {
private static final Logger _log = LoggerFactory.getLogger(Base64TokenEncoder.class);
public static final long VIPR_ENCODING_VERSION = 1000L; // 1.0.0.0
@Autowired(required = false)
TokenEncodingDisabler _tokenEncodingDisabler;
@Autowired
private TokenKeyGenerator _keyGenerator;
@Autowired
private CoordinatorClient _coordinator;
@Autowired
private InterVDCTokenCacheHelper interVDCTokenCacheHelper;
@Autowired
private GeoClientCacheManager geoClientCacheMgt;
private final GenericSerializer _serializer = new GenericSerializer();
/**
* Initialization method to be called by authsvc
*
* @throws Exception
*/
public void managerInit() throws Exception {
_keyGenerator.setCoordinator(_coordinator);
_keyGenerator.globalInit();
}
/**
* Initialization method to be called by apisvc, syssvc and any token validation apis.
*
* @throws Exception
*/
public void validatorInit() throws Exception {
_keyGenerator.setCoordinator(_coordinator);
_keyGenerator.cacheInit();
}
public void setCoordinator(CoordinatorClient coordinator) {
_coordinator = coordinator;
}
public void setTokenKeyGenerator(TokenKeyGenerator gen) {
_keyGenerator = gen;
}
public void setInterVDCTokenCacheHelper(InterVDCTokenCacheHelper helper) {
interVDCTokenCacheHelper = helper;
}
/**
* Takes a TokenOnwire class, serializes it, generates a signature based on
* a key, and serialized token, combines the serialized token and its
* signature. Serialize that, base64 encodes it and returns it as a String.
*
*/
@Override
public String encode(TokenOnWire tw) {
try {
// if encoding is disabled, just return the token id as a string
if (_tokenEncodingDisabler != null) {
return tw.getTokenId().toString();
}
// obtain key artifacts
KeyIdKeyPair pair = tw.isProxyToken() ? _keyGenerator.getProxyTokenSignatureKeyPair() :
_keyGenerator.getCurrentTokenSignatureKeyPair();
tw.setEncryptionKeyId(pair.getEntry());
// serialize token
byte[] rawTokenBytes = _serializer.toByteArray(TokenOnWire.class, tw);
// sign serialized token
String signature = SignatureHelper.sign2(rawTokenBytes, pair.getKey(),
TOKEN_SIGNING_ALGO);
// serialize/base64 encode final String.
SignedToken st = new SignedToken(rawTokenBytes, signature);
byte[] rawSignedTokenBytes = _serializer.toByteArray(SignedToken.class, st);
byte[] encodedBytes = Base64.encodeBase64(rawSignedTokenBytes);
return new String(encodedBytes, "UTF-8");
} catch (Exception ex) {
throw APIException.unauthorized.unableToEncodeToken(ex);
}
}
/**
* base64 decodes the supplied String into a SignedToken object, extracts the signature,
* deserializes the TokenOnWire object and reads its key id. Fetches the key id from the
* key generator. Computes a signature based on the fetched key and the serialized token.
* Compares the resulting signature to the one included in the SignedToken object. If
* they don't match, throw. if they do, return the deserialized TokenOnWire object.
*
* @throws SecurityException
*/
@Override
public TokenOnWire decode(String encodedToken) throws SecurityException {
try {
// if encoding is disabled, just parse the string as a token id directly
if (_tokenEncodingDisabler != null) {
return new TokenOnWire(URI.create(encodedToken));
}
// decode the bytes from the string
byte[] decoded = Base64.decodeBase64(encodedToken.getBytes("UTF-8"));
// Get the signedtoken object from the decoded bytes.
SignedToken st = _serializer.fromByteArray(SignedToken.class, decoded);
// deserialize the TokenOnWire object
TokenOnWire tw = _serializer.fromByteArray(TokenOnWire.class, st.getTokenBody());
// At this point, we know if this token was issued by this VDC or not.
// If not our VDC, check the cache for keys, if not found, make a request.
SecretKey foreignKey = null;
String vdcId = URIUtil.parseVdcIdFromURI(tw.getTokenId());
if (vdcId == null) {
_log.info("Old token from ViPR 1.1 - treating token as local");
} else if (!tw.isProxyToken() &&
!VdcUtil.getLocalShortVdcId().equals(vdcId)) {
foreignKey = getForeignKey(tw, encodedToken);
} else {
_log.info("Token VDCid {} matches that of this VDC {}", vdcId, VdcUtil.getLocalShortVdcId());
}
// get the key id that the token was encoded with
String key = tw.getEncryptionKeyId();
// fetch the corresponding key from the key generator. if the key has been rotated out, we are done.
SecretKey skey = foreignKey == null ? _keyGenerator.getTokenSignatureKey(key) : foreignKey;
if (skey == null) {
String error = String.format("The key id %s provided in the token could not be matched to secret key", key);
_log.error(error);
throw SecurityException.fatals.keyIDCouldNotBeMatchedToSecretKey(key);
}
// The key is still present in the system. Compute a signature with the fetched key against
// the body of the supplied token
String signatureFromToken = st._signature;
String computedSignature = SignatureHelper.sign2(st._tokenBody, skey,
TOKEN_SIGNING_ALGO);
// compare the computed signature against the supplied one.
if (!signatureFromToken.equals(computedSignature)) {
String error = String.format("The signature on the provided token does not validate");
_log.error(error);
throw APIException.unauthorized
.unableToDecodeTokenTheSignatureDoesNotValidate();
}
return tw;
} catch (Exception ex) {
throw APIException.unauthorized.unableToDecodeToken(ex);
}
}
/**
* Attempts to get a secret key from the provided tokenonwire.
* First attempts from cache, then makes a call to originator vdc if not
* found in cache.
*
* @param tw
* @param encodedToken
* @return
*/
private SecretKey getForeignKey(TokenOnWire tw, String encodedToken) {
String vdcId = URIUtil.parseVdcIdFromURI(tw.getTokenId());
_log.info("Token received from another VDC: {}. Looking in cache for keys", vdcId);
SecretKey foreignKey = interVDCTokenCacheHelper.getForeignSecretKey(vdcId, tw.getEncryptionKeyId());
if (foreignKey == null) {
TokenKeysBundle bundle = interVDCTokenCacheHelper.getTokenKeysBundle(vdcId);
try {
// check if the requested key id falls within reasonable range
if (bundle != null && !interVDCTokenCacheHelper.
sanitizeRequestedKeyIds(bundle, tw.getEncryptionKeyId())) {
return null;
}
TokenResponse response = geoClientCacheMgt.getGeoClient(vdcId)
.getToken(encodedToken,
bundle == null ? "0" : bundle.getKeyEntries().get(0),
bundle == null ? "0" : bundle.getKeyEntries().size() == 2 ?
bundle.getKeyEntries().get(1) : null);
if (response != null) {
TokenResponseArtifacts artifacts = TokenResponseBuilder.parseTokenResponse(response);
interVDCTokenCacheHelper.cacheForeignTokenAndKeys(artifacts, vdcId);
return interVDCTokenCacheHelper.getForeignSecretKey(vdcId, tw.getEncryptionKeyId());
} else {
_log.error("Null response from getForeignToken call. It's possible remote vdc is not reachable.");
}
} catch (Exception e) {
_log.error("Could not validate foreign token ", e);
}
} else {
_log.info("Key found in cache");
}
return foreignKey;
}
/**
* Class to encapsulate the token body as a byte array + signature concatenation
*/
public static class SignedToken {
// for ViPR versioning
private long _tokenEncodingVersion = VIPR_ENCODING_VERSION;
private byte[] _tokenBody;
private String _signature;
/**
* for deserialization
*/
public SignedToken() {
}
/**
* Creates a signed token with the body and signature provided
*
* @param body
* @param signature
*/
// Not an real issue as no write op in class
public SignedToken(final byte[] body, String signature) { // NOSONAR
// ("Suppressing: The user-supplied array 'body' is stored directly")
_tokenBody = body;
_signature = signature;
}
/**
* Encoding version is fixed, will used for backward compatibility
*
* @return version
*/
@SerializationIndex(2)
public long getTokenEncodingVersion() {
return _tokenEncodingVersion;
}
// setter is there because of property based deserialization
public void setTokenEncodingVersion(long v) {
_tokenEncodingVersion = v;
}
/**
* gets the token body
*
* @return
*/
@SerializationIndex(3)
public byte[] getTokenBody() {
// Not an real issue as no write op outside
return _tokenBody; // NOSONAR ("Suppressing: Returning '_tokenBody' may expose an internal array.")
}
/**
* sets the token body
*
* @param body
*/
// Not an real issue as no write op in class
public void setTokenBody(byte[] body) { // NOSONAR ("Suppressing: The user-supplied array 'body' is stored directly")
_tokenBody = body;
}
/**
* gets the signature
*
* @return
*/
@SerializationIndex(4)
public String getSignature() {
return _signature;
}
/**
* sets the signature
*
* @param signature
*/
public void setSignature(String signature) {
_signature = signature;
}
}
}