/*
*
* Code derived and adapted from the Jitsi client side STUN framework.
*
* Distributable under LGPL license.
* See terms of license at gnu.org.
*/
package org.restcomm.media.stun.messages.attributes.general;
import java.util.Arrays;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import org.restcomm.media.stun.StunException;
import org.restcomm.media.stun.messages.attributes.StunAttribute;
/**
* The MESSAGE-INTEGRITY attribute contains an HMAC-SHA1 [RFC2104] of the STUN
* message. The MESSAGE-INTEGRITY attribute can be present in any STUN message
* type. Since it uses the SHA1 hash, the HMAC will be 20 bytes. The text used
* as input to HMAC is the STUN message, including the header, up to and
* including the attribute preceding the MESSAGE-INTEGRITY attribute. With the
* exception of the FINGERPRINT attribute, which appears after
* MESSAGE-INTEGRITY, agents MUST ignore all other attributes that follow
* MESSAGE-INTEGRITY. The key for the HMAC depends on whether long-term or
* short-term credentials are in use. For long-term credentials, the key is 16
* bytes:
* <p>
*
* <pre>
* key = MD5(username ":" realm ":" SASLprep(password))
* </pre>
*
* That is, the 16-byte key is formed by taking the MD5 hash of the result of
* concatenating the following five fields: (1) the username, with any quotes
* and trailing nulls removed, as taken from the USERNAME attribute (in which
* case SASLprep has already been applied); (2) a single colon; (3) the realm,
* with any quotes and trailing nulls removed; (4) a single colon; and (5) the
* password, with any trailing nulls removed and after processing using
* SASLprep. For example, if the username was 'user', the realm was 'realm', and
* the password was 'pass', then the 16-byte HMAC key would be the result of
* performing an MD5 hash on the string 'user:realm:pass', the resulting hash
* being 0x8493fbc53ba582fb4c044c456bdc40eb.
* <p>
* For short-term credentials:
* <p>
*
* <pre>
* key = SASLprep(password)
* </pre>
*
* where MD5 is defined in RFC 1321 [RFC1321] and SASLprep() is defined in RFC
* 4013 [RFC4013].
* <p>
* The structure of the key when used with long-term credentials facilitates
* deployment in systems that also utilize SIP. Typically, SIP systems utilizing
* SIP's digest authentication mechanism do not actually store the password in
* the database. Rather, they store a value called H(A1), which is equal to the
* key defined above.
* <p>
* Based on the rules above, the hash used to construct MESSAGE- INTEGRITY
* includes the length field from the STUN message header. Prior to performing
* the hash, the MESSAGE-INTEGRITY attribute MUST be inserted into the message
* (with dummy content). The length MUST then be set to point to the length of
* the message up to, and including, the MESSAGE-INTEGRITY attribute itself, but
* excluding any attributes after it. Once the computation is performed, the
* value of the MESSAGE-INTEGRITY attribute can be filled in, and the value of
* the length in the STUN header can be set to its correct value -- the length
* of the entire message. Similarly, when validating the MESSAGE-INTEGRITY, the
* length field should be adjusted to point to the end of the MESSAGE-INTEGRITY
* attribute prior to calculating the HMAC. Such adjustment is necessary when
* attributes, such as FINGERPRINT, appear after MESSAGE-INTEGRITY.
*/
public class MessageIntegrityAttribute extends StunAttribute implements ContextDependentAttribute {
public static final String NAME = "MESSAGE_INTEGRITY";
public static final String HMAC_SHA1_ALGORITHM = "HmacSHA1";
public static final char DATA_LENGTH = 20;
private byte[] hmacSha1Content;
private String username;
private String media;
/**
* If the attribute belongs to a Request Message, then its the remote user
* key.<br>
* If belongs to a Response Message, then its the local user key.
*/
byte[] key;
public MessageIntegrityAttribute() {
super(StunAttribute.MESSAGE_INTEGRITY);
}
public byte[] getKey() {
return key;
}
public void setKey(byte[] key) {
this.key = key;
}
public byte[] getHmacSha1Content() {
return hmacSha1Content;
}
public void setHmacSha1Content(byte[] hmacSha1Content) {
this.hmacSha1Content = hmacSha1Content;
}
public String getUsername() {
return username;
}
/**
* Sets the username that we should use to obtain an encryption key
* (password) that the {@link #encode()} method should use when creating the
* content of this message.
*
* @param username
* the username that we should use to obtain an encryption key
* (password) that the {@link #encode()} method should use when
* creating the content of this message.
*/
public void setUsername(String username) {
this.username = username;
}
public String getMedia() {
return media;
}
/**
* Sets the media name that we should use to get the corresponding remote
* key (short-term authentication only).
*
* @param media
* name
*/
public void setMedia(String media) {
this.media = media;
}
/**
* Encodes <tt>message</tt> using <tt>key</tt> and the HMAC-SHA1 algorithm
* as per RFC 2104 and returns the resulting byte array. This is a utility
* method that generates content for the {@link MessageIntegrityAttribute}
* regardless of the credentials being used (short or long term).
*
* @param message
* the STUN message that the resulting content will need to
* travel in.
* @param offset
* the index where data starts in <tt>message</tt>.
* @param length
* the length of the data in <tt>message</tt> that the method
* should consider.
* @param key
* the key that we should be using for the encoding (which
* depends on whether we are using short or long term
* credentials).
*
* @return the HMAC that should be used in a
* <tt>MessageIntegrityAttribute</tt> transported by
* <tt>message</tt>.
*
* @throws IllegalArgumentException
* if the encoding fails for some reason.
*/
public static byte[] calculateHmacSha1(byte[] message, int offset, int length, byte[] key) throws IllegalArgumentException {
try {
// get an HMAC-SHA1 key from the raw key bytes
SecretKeySpec signingKey = new SecretKeySpec(key, HMAC_SHA1_ALGORITHM);
// get an HMAC-SHA1 Mac instance and initialize it with the key
Mac mac = Mac.getInstance(HMAC_SHA1_ALGORITHM);
mac.init(signingKey);
// compute the hmac on input data bytes
byte[] macInput = new byte[length];
System.arraycopy(message, offset, macInput, 0, length);
return mac.doFinal(macInput);
} catch (Exception exc) {
throw new IllegalArgumentException("Could not create HMAC-SHA1 request encoding", exc);
}
}
public byte[] encode(byte[] data, int offset, int length) {
char type = getAttributeType();
byte binValue[] = new byte[HEADER_LENGTH + getDataLength()];
// Type
binValue[0] = (byte) (type >> 8);
binValue[1] = (byte) (type & 0x00FF);
// Length
binValue[2] = (byte) (getDataLength() >> 8);
binValue[3] = (byte) (getDataLength() & 0x00FF);
char msgType = (char) ((data[0] << 8) + data[1]);
// now calculate the HMAC-SHA1
this.hmacSha1Content = calculateHmacSha1(data, offset, length, this.key);
// username
System.arraycopy(hmacSha1Content, 0, binValue, 4, getDataLength());
return binValue;
}
@Override
public char getDataLength() {
return DATA_LENGTH;
}
@Override
public String getName() {
return NAME;
}
@Override
public boolean equals(Object other) {
if (other == null || !(other instanceof MessageIntegrityAttribute)) {
return false;
}
if (other == this) {
return true;
}
MessageIntegrityAttribute att = (MessageIntegrityAttribute) other;
if (att.getAttributeType() != getAttributeType() || att.getDataLength() != getDataLength() || !Arrays.equals(att.hmacSha1Content, hmacSha1Content)) {
return false;
}
return true;
}
@Override
public byte[] encode() throws UnsupportedOperationException {
throw new UnsupportedOperationException("ContentDependentAttributes should be encoded through the contend-dependent encode method");
}
@Override
protected void decodeAttributeBody(byte[] data, char offset, char length) throws StunException {
this.hmacSha1Content = new byte[length];
System.arraycopy(data, offset, this.hmacSha1Content, 0, length);
}
@Override
public String toString() {
StringBuilder builder = new StringBuilder(NAME).append(": ");
builder.append("username=").append(this.username).append(", ");
builder.append("key=").append(this.key);
return builder.toString();
}
}