I am investigating SafetyNet provided by Google within my Android Application.
To start with I simply called the SafetyNet attest API and Base64 decoded the parts as shown in the Google supplied examples.
SafetyNet.getClient(this).attest(NONCE, <API KEY>)
.addOnSuccessListener(this, new OnSuccessListener<SafetyNetApi.AttestationResponse>() {
@Override
public void onSuccess(final SafetyNetApi.AttestationResponse attestationResponse) {
initialDataExtraction(attestationResponse.getJwsResult());
}
})
.addOnFailureListener(this, new OnFailureListener() {
@Override
public void onFailure(@NonNull final Exception exception) {
if (exception instanceof ApiException) {
final ApiException apiException = (ApiException) exception;
Log.e(TAG, "onFailure: " + apiException.getMessage() + " " + apiException.getStatusCode());
} else {
Log.e(TAG, "Error: ", exception);
}
}
});
I extract the JWS parts as follows:-
private byte[] initialDataExtraction(final String jwsResult) {
final String[] jwsResultParts = jwsResult.split("[.]");
if (jwsResultParts.length == 3) {
final byte[] header = Base64.decode(jwsResultParts[0], Base64.NO_WRAP);
final byte[] data = Base64.decode(jwsResultParts[1], Base64.NO_WRAP);
final byte[] signature = Base64.decode(jwsResultParts[2], Base64.NO_WRAP);
Log.d(TAG, "initialDataExtraction: header = " + new String(header, UTF_8));
Log.d(TAG, "initialDataExtraction: data = " + new String(data, UTF_8));
Log.d(TAG, "initialDataExtraction: signature = " + new String(signature, UTF_8));
return data;
} else {
Log.e(TAG, "initialDataExtraction: Failure: Illegal JWS signature format. The JWS consists of " + jwsResultParts.length + " parts instead of 3.");
return null;
}
}
I am using android.util.Base64
to decode the parts and the majority of the time the decoding completes OK.
Occasionally I receive this exception though:-
java.lang.IllegalArgumentException: bad base-64
at android.util.Base64.decode(Base64.java:161)
at android.util.Base64.decode(Base64.java:136)
at android.util.Base64.decode(Base64.java:118)
when decoding the Signature part.
What am I doing wrong when decoding to see this intermittent error?
I then moved onto to using a JWT library to decode the tokens.
first I tried group: 'com.auth0.android', name: 'jwtdecode', version: '1.1.1'
the code I tried is
final JWT jwt = new JWT(jwsResult);
which consistently fails with the following error
com.auth0.android.jwt.DecodeException: The token's payload had an invalid JSON format.
at com.auth0.android.jwt.JWT.parseJson(JWT.java:235)
at com.auth0.android.jwt.JWT.decode(JWT.java:203)
at com.auth0.android.jwt.JWT.<init>(JWT.java:40)
Caused by: com.google.gson.JsonSyntaxException: java.lang.IllegalStateException: Expected a string but was BEGIN_ARRAY at line 1 column 23 path $.
at com.google.gson.Gson.fromJson(Gson.java:899)
at com.google.gson.Gson.fromJson(Gson.java:852)
at com.google.gson.Gson.fromJson(Gson.java:801)
This exception seems to be caused by the Auth0 library being unable to parse headers 4.1.6. "x5c" (X.509 Certificate Chain) Header
format which is odd as the JWS Spec clearly states the value is represented by a JSON aray:-
The "x5c" (X.509 Certificate Chain) Header Parameter contains the
X.509 public key certificate or certificate chain [RFC5280]
corresponding to the key used to digitally sign the JWS. The
certificate or certificate chain is represented as a JSON array of
certificate value strings.
However If I copy and paste the same jws result string into a pure java project and use compile 'com.auth0:java-jwt:3.3.0'
and use this code:-
String token = "<JWS TOKEN>";
try {
final DecodedJWT jwt = JWT.decode(token);
System.out.println("Header = " + jwt.getHeader());
System.out.println("Payload = " + jwt.getPayload());
System.out.println("Signature = " + jwt.getSignature());
} catch (JWTDecodeException exception){
throw new RuntimeException(exception);
}
The Jws Token is decoded successfully.
What am I doing wrong within my Android application that stops the auth0 android jwt library working as desired?
I then tried 'io.jsonwebtoken:jjwt:0.9.0'
library within my Android application.
When I execute this code:-
Jwts.parser().parse(jwsResult).getBody();
it fails with:-
java.lang.IllegalArgumentException: A signing key must be specified if the specified JWT is digitally signed.
at io.jsonwebtoken.lang.Assert.notNull(Assert.java:85)
at io.jsonwebtoken.impl.DefaultJwtParser.parse(DefaultJwtParser.java:331)
What signing key do I need to pass to Jwts? The only key I have is my API key held in the Google API Console, is this the key I should employ?
when I pass it as follows:
Jwts.parser().setSigningKey<API KEY>.parse(jwsResult).getBody();
this fails with:-
java.lang.IllegalArgumentException: Key bytes can only be specified for HMAC signatures. Please specify a PublicKey or PrivateKey instance.
at io.jsonwebtoken.lang.Assert.isTrue(Assert.java:38)
at io.jsonwebtoken.impl.DefaultJwtParser.parse(DefaultJwtParser.java:324)
What is the correct approach to decode and consume the Jws result received from SafetyNet attest API call?
I discovered a fix for the java.lang.IllegalArgumentException: bad base-64
issue from this question Base64: java.lang.IllegalArgumentException: Illegal character
simply replace characters in jws token before decoding
token.replace('-', '+').replace('_', '/')
I identified this library not only does it do the job it works fine on Android.
// https://mvnrepository.com/artifact/com.nimbusds/nimbus-jose-jwt
implementation group: 'com.nimbusds', name: 'nimbus-jose-jwt', version: '5.1'
try {
final JWSObject jwsObject = JWSObject.parse(jwsResult);
System.out.println("header = " + jwsObject.getHeader());
System.out.println("header = " + jwsObject.getHeader().getX509CertChain());
System.out.println("payload = " + jwsObject.getPayload().toJSONObject());
System.out.println("signature = " + jwsObject.getSignature());
System.out.println("signature = " + jwsObject.getSignature().decodeToString());
} catch (ParseException e) {
e.printStackTrace();
}
Some nice examples are provided:-