package com.ericsson.ssa.container;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.InetSocketAddress;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.StringTokenizer;
import javax.crypto.KeyGenerator;
import javax.crypto.Mac;
import javax.crypto.SecretKey;
import org.apache.catalina.util.Base64;
import org.jvnet.glassfish.comms.util.LogUtil;
import com.ericsson.ssa.sip.dns.SipTransports;
import com.ericsson.ssa.sip.dns.TargetTuple;
/**
* FlowToken class used for connection reuse.
* FlowToken is representing a flowtoken used to identify the flow to the UA.
* The flow to UA is used for sending responses and subsequent request.
*
* The flowtoken is generated so it can be correlated to specific connection information where the request was recieved on.
* The flowtoken is obfuscated for security reasons before it leaves the container.
*
* @author epkadsz, Adrian Szwej
* @version 1.0, 2008-09-10
*
*/
public class FlowToken {
/** Reused flowtoken bytearray. */
private static final ByteArrayOutputStream flowTokenOutput = new ByteArrayOutputStream();
/** Size of the digest included in the flowtoken before it gets base64 encoded. */
private static final int DIGEST_LENGTH = 20;
/** Prepared HMAC generator with a secret key. */
private static Mac HMAC;
private static final char SEP = ':';
static {
try {
// Generate a key for the HMAC-SHA1-80 keyed-hashing algorithm; see RFC 2104
KeyGenerator keyGen = KeyGenerator.getInstance("HmacSHA1");
SecretKey key = keyGen.generateKey();
// Create a MAC object using HMAC-SHA1-80 and initialize with key
HMAC = Mac.getInstance(key.getAlgorithm());
HMAC.init(key);
} catch (NoSuchAlgorithmException e1) {
LogUtil.SIP_LOGGER.getLogger().severe("Missing HmacSHA1 algorithm");
} catch (InvalidKeyException e) {
LogUtil.SIP_LOGGER.getLogger().severe("Cannot generate secret key. Path header during outbound proxy will not be obfuscated.");
}
}
/** Base64 encoded flow id. */
private String flowId;
/**
* Constructor
* @param remotePoint the UA endpoint
* @param localPoint the container endpoint
*/
public FlowToken(TargetTuple remotePoint, InetSocketAddress localPoint) {
this.flowId = encodeToken(composeConnectionInfo(remotePoint, localPoint));
}
/**
* Constructor
* @param flow token extracted directly from the Path header.
*/
public FlowToken(String flowToken) {
this.flowId = flowToken;
}
public String toString() {
return flowId;
}
/**
* Gets the flow identifier.
* @return Base64 encoded flowIf containing the connection information with its HMAC.
*/
public String getEncodedFlowId() {
return flowId;
}
/**
* Gets the connection information forming this flow.
* The remote is the UA address connecting the connection to the container.
* @return the remote tuple, null if there is a problem to decode the information
*/
public TargetTuple getRemote() {
StringTokenizer stringToken = new StringTokenizer(decodeToken(getEncodedFlowId()), String.valueOf(SEP));
String transport = stringToken.nextToken();
String remoteIP = stringToken.nextToken();
String remotePort = stringToken.nextToken();
try {
return new TargetTuple(SipTransports.getTransport(transport), remoteIP, Integer.parseInt(remotePort));
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
/**
* Validates the token by extracting the HMAC prefix and comparing against the HMAC calculated from the connection information.
*
* 1) base64 decode the flowId.
* 2) The first 20 octets are HMAC calculated value over the remaining part of the decoded string
*
* @throws Exception when flowtoken is tampered
*/
public void validate() throws Exception {
if (HMAC != null) {
byte[] decodedDigestB64 = Base64.decode(flowId.getBytes("UTF8"));
if (decodedDigestB64.length <= DIGEST_LENGTH)
throw new Exception("Flowtoken has been tampered. Flowtoken is too short.");
byte[] messageCandidate = new byte[decodedDigestB64.length - DIGEST_LENGTH];
System.arraycopy(decodedDigestB64, DIGEST_LENGTH, messageCandidate, 0, decodedDigestB64.length - DIGEST_LENGTH);
byte[] calculatedDigest = calculateDigest(messageCandidate);
for (int i = 0; i < DIGEST_LENGTH; i++)
if (calculatedDigest[i] != decodedDigestB64[i])
throw new Exception("Flowtoken has been tampered. Flowtoken missmatch");
}
}
/**
* Generates string token with encoded information.
* 1) the HMAC is computed using the input together with the secret key.
* 2) the HMAC and the input is concatenated on format: HMAC + input (where HMAC corresponds to first 20 octets)
* 3) The result is base64 encoded and returned
*
* @param input used for token generation
* @return base64 encoded string
**/
private static String encodeToken(String input) {
// Encode the string into bytes using utf-8 and digest it
if (HMAC != null) {
byte[] flowToken;
byte[] utf8 = null;
byte[] digest = null;
try {
utf8 = input.getBytes("UTF8");
digest = calculateDigest(utf8);
} catch (UnsupportedEncodingException e1) {
}
synchronized (flowTokenOutput) {
flowTokenOutput.reset();
try {
flowTokenOutput.write(digest);
flowTokenOutput.write(utf8);
} catch (IOException e) {
}
flowToken = flowTokenOutput.toByteArray();
}
return new String(Base64.encode(flowToken));
}
else
return input;
}
private static byte[] calculateDigest(byte[] utf8) {
synchronized (HMAC) {
return HMAC.doFinal(utf8);
}
}
/**
* Encodes connection information on format:
* protocol + remote IP address + remote port + local IP address + local port +
* @return
*/
private static String composeConnectionInfo(TargetTuple remotePoint, InetSocketAddress localPoint) {
return remotePoint.getProtocol().name() + SEP + remotePoint.getIP() + SEP + remotePoint.getPort() + SEP + localPoint.getHostName() + SEP + localPoint.getPort();
}
/**
* Decodes the encoded token. This method does 64 bit decode, and strips of the HMAC information.
* @param encodedToken 64 base encoded token with HMAC
* @return 64 decoded string without the HMAC prefix
*/
private static String decodeToken(String encodedToken) {
//remove the first digits
if (HMAC != null) {
try {
byte[] decodedDigestB64 = Base64.decode(encodedToken.getBytes("UTF8"));
byte[] messageCandidate = new byte[decodedDigestB64.length - DIGEST_LENGTH];
System.arraycopy(decodedDigestB64, DIGEST_LENGTH, messageCandidate, 0, decodedDigestB64.length - DIGEST_LENGTH);
return new String(messageCandidate, "UTF8");
}
catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
}
return null;
}
}