Skip to content

Commit

Permalink
Merge pull request #39 from efrohnhoefer/add_HMACSHA256_support
Browse files Browse the repository at this point in the history
Add support for HMAC-SHA256 signature method
  • Loading branch information
vinodmehra committed Aug 22, 2017
2 parents 7bea873 + 834241a commit b4f6afb
Show file tree
Hide file tree
Showing 8 changed files with 94 additions and 18 deletions.
6 changes: 3 additions & 3 deletions pom.xml
Expand Up @@ -20,12 +20,12 @@
<dependency>
<groupId>org.scala-lang</groupId>
<artifactId>scala-library</artifactId>
<version>2.9.2</version>
<version>2.10.6</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.scala-tools.testing</groupId>
<artifactId>specs_2.9.1</artifactId>
<artifactId>specs_2.10</artifactId>
<version>1.6.9</version>
<scope>test</scope>
</dependency>
Expand Down Expand Up @@ -75,7 +75,7 @@
<maven.compiler.target>1.6</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<encoding>UTF-8</encoding>
<scala.version>2.9.2</scala.version>
<scala.version>2.10.6</scala.version>
<slf4j.version>1.7.5</slf4j.version>
</properties>

Expand Down
1 change: 1 addition & 0 deletions src/main/java/com/twitter/joauth/OAuthParams.java
Expand Up @@ -42,6 +42,7 @@ public class OAuthParams {
public static final String UNSET = "(unset)";

public static final String HMAC_SHA1 = "HMAC-SHA1";
public static final String HMAC_SHA256 = "HMAC-SHA256";
public static final String ONE_DOT_OH = "1.0";
public static final String ONE_DOT_OH_A = "1.0a";

Expand Down
40 changes: 31 additions & 9 deletions src/main/java/com/twitter/joauth/Signer.java
Expand Up @@ -29,13 +29,29 @@ public abstract class Signer {
/**
* produce an encoded signature string
*/
public abstract String getString(String str, String tokenSecret, String consumerSecret)
public String getString(String str, String tokenSecret, String consumerSecret)
throws InvalidKeyException, NoSuchAlgorithmException {
return getString(str, OAuthParams.HMAC_SHA1, tokenSecret, consumerSecret);
}

/**
* produce an encoded signature string
*/
public abstract String getString(String str, String signatureMethod, String tokenSecret, String consumerSecret)
throws InvalidKeyException, NoSuchAlgorithmException;

/**
* produce a signature as a byte array
*/
public abstract byte[] getBytes(String str, String tokenSecret, String consumerSecret)
public byte[] getBytes(String str, String tokenSecret, String consumerSecret)
throws NoSuchAlgorithmException, InvalidKeyException {
return getBytes(str, OAuthParams.HMAC_SHA1, tokenSecret, consumerSecret);
}

/**
* produce a signature as a byte array
*/
public abstract byte[] getBytes(String str, String signatureMethod, String tokenSecret, String consumerSecret)
throws NoSuchAlgorithmException, InvalidKeyException;

/**
Expand All @@ -59,23 +75,25 @@ public static class StandardSigner extends Signer {
private static final Charset UTF_8 = Charset.forName("UTF-8");
private static final String AND = "&"; //TODO: move to Normalizer
private static final String HMACSHA1 = "HmacSHA1";
private static final String HMACSHA256 = "HmacSHA256";

@Override
public String getString(String str, String tokenSecret, String consumerSecret)
public String getString(String str, String signatureMethod, String tokenSecret, String consumerSecret)
throws InvalidKeyException, NoSuchAlgorithmException {

return UrlCodec.encode(Base64Util.encode(getBytes(str, tokenSecret, consumerSecret)));
return UrlCodec.encode(Base64Util.encode(getBytes(str, signatureMethod, tokenSecret, consumerSecret)));
}

@Override
public byte[] getBytes(String str, String tokenSecret, String consumerSecret)
public byte[] getBytes(String str, String signatureMethod, String tokenSecret, String consumerSecret)
throws NoSuchAlgorithmException, InvalidKeyException {

String algorithm = getSignerAlgorithm(signatureMethod);
String key = consumerSecret + AND + tokenSecret;
SecretKeySpec signingKey = new SecretKeySpec(key.getBytes(StandardSigner.UTF_8), HMACSHA1);
SecretKeySpec signingKey = new SecretKeySpec(key.getBytes(UTF_8), algorithm);

//TODO: Mac looks thread safe, if not consider synchronizing this
Mac mac = Mac.getInstance(HMACSHA1);
Mac mac = Mac.getInstance(algorithm);
mac.init(signingKey);
return mac.doFinal(str.getBytes(UTF_8));
}
Expand All @@ -84,6 +102,10 @@ public byte[] getBytes(String str, String tokenSecret, String consumerSecret)
public byte[] toBytes(String signature) throws UnsupportedEncodingException {
return Base64Util.decode(UrlCodec.decode(signature).trim());
}

String getSignerAlgorithm(String signatureMethod) {
return OAuthParams.HMAC_SHA256.equals(signatureMethod) ? HMACSHA256 : HMACSHA1;
}
}

/**
Expand All @@ -100,12 +122,12 @@ public ConstSigner(String str, byte[] bytes) {
}

@Override
public byte[] getBytes(String str, String tokenSecret, String consumerSecret) {
public byte[] getBytes(String str, String signatureMethod, String tokenSecret, String consumerSecret) {
return bytes;
}

@Override
public String getString(String str, String tokenSecret, String consumerSecret) {
public String getString(String str, String signatureMethod, String tokenSecret, String consumerSecret) {
return str;
}

Expand Down
4 changes: 3 additions & 1 deletion src/main/java/com/twitter/joauth/UnpackedRequest.java
Expand Up @@ -354,7 +354,9 @@ public void verify(Request.ParsedRequest parsedRequest, OAuthParams.OAuth1Params
else if (parsedRequest.port() < 0) throwMalformedException(PORT);
else if (parsedRequest.verb() == null) throwMalformedException(VERB);
else if (parsedRequest.path() == null) throwMalformedException(PATH);
else if (oAuth1Params.signatureMethod() == null || !oAuth1Params.signatureMethod().equals(OAuthParams.HMAC_SHA1)) {
else if (oAuth1Params.signatureMethod() == null ||
!oAuth1Params.signatureMethod().equals(OAuthParams.HMAC_SHA1) &&
!oAuth1Params.signatureMethod().equals(OAuthParams.HMAC_SHA256)) {
throw new MalformedRequest(UNSUPPORTED_METHOD + oAuth1Params.signatureMethod());
}
else if (oAuth1Params.version() != null &&
Expand Down
8 changes: 6 additions & 2 deletions src/main/java/com/twitter/joauth/Verifier.java
Expand Up @@ -108,6 +108,7 @@ public VerifierResult verify(UnpackedRequest.OAuth1TwoLeggedRequest request, Str
"",
consumerSecret,
request.signature(),
request.signatureMethod(),
request.normalizedRequest()
);
}
Expand All @@ -121,6 +122,7 @@ public VerifierResult verify(UnpackedRequest.OAuth1Request request, String token
tokenSecret,
consumerSecret,
request.signature(),
request.signatureMethod(),
request.normalizedRequest()
);
}
Expand All @@ -132,6 +134,7 @@ private VerifierResult verifyOAuth1(
String tokenSecret,
String consumerSecret,
String signature,
String signatureMethod,
String normalizedRequest
) {
if (!validateTimestampSecs(timestampSecs)) {
Expand All @@ -144,7 +147,7 @@ private VerifierResult verifyOAuth1(
log.log(Level.FINE, String.format("bad nonce -> %s", request.toString()));
}
return VerifierResult.BAD_NONCE;
} else if (!validateSignature(normalizedRequest, signature, tokenSecret, consumerSecret)) {
} else if (!validateSignature(normalizedRequest, signature, signatureMethod, tokenSecret, consumerSecret)) {
if (log.isLoggable(Level.FINE)) {
log.log(Level.FINE, String.format("bad signature -> %s", request.toString()));
}
Expand All @@ -164,12 +167,13 @@ public boolean validateTimestampSecs(long timestampSecs) {
boolean validateSignature(
String normalizedRequest,
String signature,
String signatureMethod,
String tokenSecret,
String consumerSecret
) {
try {
return Base64Util.equals(UrlCodec.decode(signature).trim(),
signer.getBytes(normalizedRequest, tokenSecret, consumerSecret));
signer.getBytes(normalizedRequest, signatureMethod, tokenSecret, consumerSecret));
} catch (Exception e) {
return false;
}
Expand Down
10 changes: 10 additions & 0 deletions src/test/scala/com/twitter/joauth/OAuth1RequestSpec.scala
Expand Up @@ -43,6 +43,16 @@ class OAuth1RequestSpec extends SpecificationWithJUnit {
builder.queryHandler.handle("oauth_signature_method", "foo")
UnpackedRequest.O_AUTH_1_REQUEST_HELPER.verify(pr("1", "2", 3, "4", "5"), builder.oAuth1Params) must throwA(new MalformedRequest("unsupported signature method: foo"))
}
"not throw on supported HMAC-SHA1 signature method" in {
builder.queryHandler.handle("oauth_signature_method", "HMAC-SHA1")
UnpackedRequest.O_AUTH_1_REQUEST_HELPER.verify(pr("1", "2", 3, "4", "5"), builder.oAuth1Params)
1 must be_==(1)
}
"not throw on supported HMAC-SHA256 signature method" in {
builder.queryHandler.handle("oauth_signature_method", "HMAC-SHA256")
UnpackedRequest.O_AUTH_1_REQUEST_HELPER.verify(pr("1", "2", 3, "4", "5"), builder.oAuth1Params)
1 must be_==(1)
}
"throw on unsupported oauth version" in {
builder.queryHandler.handle("oauth_signature_method", "HMAC-SHA1")
builder.queryHandler.handle("oauth_version", "1.1")
Expand Down
34 changes: 34 additions & 0 deletions src/test/scala/com/twitter/joauth/SignerSpec.scala
Expand Up @@ -33,5 +33,39 @@ class SignerSpec extends SpecificationWithJUnit {
badbytes(bytes.length - 1) = 0: Byte
Base64Util.equals(signature, badbytes) must beFalse
}

"sign correctly with HMAC-SHA1" in {
val tokenSecret = "readsecret"
val consumerSecret = "writesecret"
val normalizedRequest = "GET&http%3A%2F%2Fphotos.example.net%2Fphotos&file%3Dvacation.jpg%26oauth_consumer_key%3Dwritekey%26oauth_nonce%3Dkllo9940pd9333jh%26oauth_signature_method%3DHMAC-SHA1%26oauth_timestamp%3D1191242096%26oauth_token%3Dreadkey%26oauth_version%3D1.0%26size%3Doriginal"
val signature = "Dq+QxkRpmASNSiUrwhCBbQYZuBo="
val signatureMethod = "HMAC-SHA1"
val bytes = Base64Util.decode(signature)
val encoded = UrlCodec.encode(signature)
signer.getString(normalizedRequest, signatureMethod, tokenSecret, consumerSecret) mustEqual encoded
Arrays.equals(signer.getBytes(normalizedRequest, signatureMethod, tokenSecret, consumerSecret), bytes) must beTrue
Arrays.equals(signer.toBytes(encoded), bytes) must beTrue
Base64Util.equals(signature, bytes) must beTrue
val badbytes = Base64Util.decode(signature)
badbytes(bytes.length - 1) = 0: Byte
Base64Util.equals(signature, badbytes) must beFalse
}

"sign correctly with HMAC-SHA256" in {
val tokenSecret = "readsecret"
val consumerSecret = "writesecret"
val normalizedRequest = "GET&http%3A%2F%2Fphotos.example.net%2Fphotos&file%3Dvacation.jpg%26oauth_consumer_key%3Dwritekey%26oauth_nonce%3Dkllo9940pd9333jh%26oauth_signature_method%3DHMAC-SHA1%26oauth_timestamp%3D1191242096%26oauth_token%3Dreadkey%26oauth_version%3D1.0%26size%3Doriginal"
val signature = "e7/frN70SVX3tjw7CGwo8iehphTeOer434AECKQsvpA="
val signatureMethod = "HMAC-SHA256"
val bytes = Base64Util.decode(signature)
val encoded = UrlCodec.encode(signature)
signer.getString(normalizedRequest, signatureMethod, tokenSecret, consumerSecret) mustEqual encoded
Arrays.equals(signer.getBytes(normalizedRequest, signatureMethod, tokenSecret, consumerSecret), bytes) must beTrue
Arrays.equals(signer.toBytes(encoded), bytes) must beTrue
Base64Util.equals(signature, bytes) must beTrue
val badbytes = Base64Util.decode(signature)
badbytes(bytes.length - 1) = 0: Byte
Base64Util.equals(signature, badbytes) must beFalse
}
}
}
9 changes: 6 additions & 3 deletions src/test/scala/com/twitter/joauth/VerifierSpec.scala
Expand Up @@ -65,18 +65,21 @@ class VerifierSpec extends SpecificationWithJUnit with Mockito {
"validateSignature" should {
"return false for malformed signature" in {
request.signature() returns "rEh%2FpUnLF9ZSV8WmIMGARQlM2VQ%3D%0"
request.signatureMethod() returns "HMAC-SHA1"
request.normalizedRequest() returns "GET&https%3A%2F%2Fapi.twitter.com%2F1.1%2Fusers%2Flookup.json&oauth_consumer_key%3Dabcd%26oauth_nonce%3Dnonce%26oauth_signature_method%3DHMAC-SHA1%26oauth_timestamp%3D1363119598%26oauth_token%3Dijkl%26oauth_version%3D1.0%26user_id%3D1234567890"
verify.validateSignature(request.normalizedRequest(), request.signature(), "readsecret", "writesecret") must beFalse
verify.validateSignature(request.normalizedRequest(), request.signature(), request.signatureMethod(), "readsecret", "writesecret") must beFalse
}
"return true for good signature" in {
request.signature() returns "rEh%2FpUnLF9ZSV8WmIMGARQlM2VQ%3D"
request.signatureMethod() returns "HMAC-SHA1"
request.normalizedRequest() returns "GET&https%3A%2F%2Fapi.twitter.com%2F1.1%2Fusers%2Flookup.json&oauth_consumer_key%3Dabcd%26oauth_nonce%3Dnonce%26oauth_signature_method%3DHMAC-SHA1%26oauth_timestamp%3D1363119598%26oauth_token%3Dijkl%26oauth_version%3D1.0%26user_id%3D1234567890"
verify.validateSignature(request.normalizedRequest(), request.signature(), "readsecret", "writesecret") must beTrue
verify.validateSignature(request.normalizedRequest(), request.signature(), request.signatureMethod(), "readsecret", "writesecret") must beTrue
}
"return false for bad signature" in {
request.signature() returns "cNwF13Zo%2FIaX8MT6QdYlJWn%2B4%2F4%3D"
request.signatureMethod() returns "HMAC-SHA1"
request.normalizedRequest() returns "GET&https%3A%2F%2Fapi.twitter.com%2F1.1%2Fusers%2Flookup.json&oauth_consumer_key%3Dabcd%26oauth_nonce%3Dnonce%26oauth_signature_method%3DHMAC-SHA1%26oauth_timestamp%3D1363119598%26oauth_token%3Dijkl%26oauth_version%3D1.0%26user_id%3D1234567890"
verify.validateSignature(request.normalizedRequest(), request.signature(), "readsecret", "writesecret") must beFalse
verify.validateSignature(request.normalizedRequest(), request.signature(), request.signatureMethod(), "readsecret", "writesecret") must beFalse
}
}

Expand Down

0 comments on commit b4f6afb

Please sign in to comment.