I wrote a method to verify a gigya signature against a specified timestamp and UID, based on Gigya\'s instructions for constructing a signature. Here is Gigya\'s psu
Code review time! I love doing these. Let's check your solution and see where we fall.
In prose, our goal is to underscore-connect a timestamp and UID together, coerce the result from UTF-8 into a byte array, coerce a given Base64 secret key to into a second byte array, SHA-1 the two byte arrays together, then convert the result back to Base64. Simple, right?
(Yes, that pseudocode has a bug.)
Let's step through your code, now:
public boolean verifyGigyaSig(String uid, String timestamp, String signature) {
Your method signature here is fine. Though obviously, you'll want to make sure your created timestamps and the ones you're verifying are using the exact same format (otherwise, this will always fail) and that your Strings are UTF-8 encoded.
(Further details about how String encodings work in Java)
// Construct the "base string"
String baseString = timestamp + "_" + uid;
// Convert the base string into a binary array
byte[] baseBytes = baseString.getBytes("UTF-8");
This is fine (reference a, reference b). But, in the future, consider using StringBuilder for String concatenation explicitly, instead of relying on compiler-time optimizations to support this feature.
Note the documentation up to this point is inconsistent on whether to use "UTF-8" or "UTF8" as your charset identifier. "UTF-8" is the accepted identifier, though; I believe "UTF8" is kept for legacy and compatibility purposes.
// Convert secretKey from BASE64 to a binary array
String secretKey = MyConfig.getGigyaSecretKey();
byte[] secretKeyBytes = Base64.decodeBase64(secretKey);
Hold it! This breaks encapsulation. It's functionally correct, but it would be better if you passed this as a parameter to your method than pulling it in from another source (thus coupling your code, in this case, to the details of MyConfig
). Otherwise, this, too, is fine.
// Use the HMAC-SHA1 algorithm to calculate the signature
Mac mac = Mac.getInstance("HmacSHA1");
mac.init(new SecretKeySpec(secretKeyBytes, "HmacSHA1"));
byte[] signatureBytes = mac.doFinal(baseBytes);
Yep, this is correct (reference a, reference b, reference c). I've nothing to add here.
// Convert the signature to a BASE64
String calculatedSignature = Base64.encodeBase64String(signatureBytes);
Correct, and...
// Return true iff constructed signature equals specified signature
return signature.equals(calculatedSignature);
}
... correct. Ignoring the caveats and implementation notes, your code checks out procedurally.
I would speculate on a few points, though:
Are you UTF-8 encoding your input String for your UID or your timestamp, as defined here? If you've failed to do so, you're not going to get the results you expect!
Are you sure the secret key is correct and properly encoded? Make sure to check this in a debugger!
For that matter, verify the whole thing in a debugger if you have access to a signature generation algorithm, in Java or otherwise. Failing this, synthesizing one will help you check your work because of the encoding caveats raised in the documentation.
The pseudocode bug should be reported, as well.
I believe checking your work here, especially your String encodings, will expose the correct solution.
I checked their implementation of Base64 against Apache Commons Codec's. Test code:
import org.apache.commons.codec.binary.Base64;
import static com.gigya.socialize.Base64.*;
import java.io.IOException;
public class CompareBase64 {
public static void main(String[] args)
throws IOException, ClassNotFoundException {
byte[] test = "This is a test string.".getBytes();
String a = Base64.encodeBase64String(test);
String b = encodeToString(test, false);
byte[] c = Base64.decodeBase64(a);
byte[] d = decode(b);
assert(a.equals(b));
for (int i = 0; i < c.length; ++i) {
assert(c[i] == d[i]);
}
assert(Base64.encodeBase64String(c).equals(encodeToString(d, false)));
System.out.println(a);
System.out.println(b);
}
}
Simple tests show that their output is comparable. Output:
dGhpcyBpcyBteSB0ZXN0IHN0cmluZw==
dGhpcyBpcyBteSB0ZXN0IHN0cmluZw==
I verified this in a debugger, just in case there might be whitespace I can't detect in visual analysis and the assert didn't hit. They're identical. I also checked a paragraph of lorem ipsum, just to be sure.
Here's the source code for their signature generator, sans Javadoc (author credit: Raviv Pavel):
public static boolean validateUserSignature(String UID, String timestamp, String secret, String signature) throws InvalidKeyException, UnsupportedEncodingException
{
String expectedSig = calcSignature("HmacSHA1", timestamp+"_"+UID, Base64.decode(secret));
return expectedSig.equals(signature);
}
private static String calcSignature(String algorithmName, String text, byte[] key) throws InvalidKeyException, UnsupportedEncodingException
{
byte[] textData = text.getBytes("UTF-8");
SecretKeySpec signingKey = new SecretKeySpec(key, algorithmName);
Mac mac;
try {
mac = Mac.getInstance(algorithmName);
} catch (NoSuchAlgorithmException e) {
return null;
}
mac.init(signingKey);
byte[] rawHmac = mac.doFinal(textData);
return Base64.encodeToString(rawHmac, false);
}
Changing your function signature in line with some of the changes I made above and running this test case causes both signatures to be validated correctly:
// Redefined your method signature as:
// public static boolean verifyGigyaSig(
// String uid, String timestamp, String secret, String signature)
public static void main(String[] args) throws
IOException,ClassNotFoundException,InvalidKeyException,
NoSuchAlgorithmException,UnsupportedEncodingException {
String uid = "10242048";
String timestamp = "imagine this is a timestamp";
String secret = "sosecure";
String signature = calcSignature("HmacSHA1",
timestamp+"_"+uid, secret.getBytes());
boolean yours = verifyGigyaSig(
uid,timestamp,encodeToString(secret.getBytes(),false),signature);
boolean theirs = validateUserSignature(
uid,timestamp,encodeToString(secret.getBytes(),false),signature);
assert(yours == theirs);
}
Of course, as reproduced, the problem is with Commons Net, whereas Commons Codec appears to be fine.