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:-
https://connect2id.com/products/nimbus-jose-jwt/examples
Related
I am trying to authenticate a "My Anime List" user using Oauth2 (following this guide) for my Android application.
Step 1: getting the authorization token
Here, I am using a WebView to prompt the user for its username and password. This step seems to work as far as I can see.
private static final String REDIRECT_URL = "http://localhost/oauth";
private static final String CLIENT_ID = "9c..."; // omitted
private static final String OAUTH_BASE_URL = "https://myanimelist.net/v1/oauth2/";
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_login);
/*
* Before you can authenticate a user, your client needs to generate a Code Verifier and a
* Code Challenge. A Code Verifier is a high-entropy, cryptographic, random string
* containing only the characters [A-Z] / [a-z] / [0-9] / "-" / "." / "_" / "~".
* The length of the string must be between 43 and 128 characters.
*
* MAL only allows the plain transformation for the Code Challenge.
* In other words, it means that you have to set the Code Challenge equal to the
* Code Verifier.
*/
String codeChallenge = PKCEGenerator.generateVerifier(128);
webview = findViewById(R.id.login_webview);
webview.getSettings().setJavaScriptEnabled(true);
webview.setWebViewClient(new WebViewClient(){
public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request){
Log.d(TAG, "Redirecting to: " + request.getUrl());
Uri url = request.getUrl();
if(url.toString().contains(REDIRECT_URL)){
String authorizationCode = url.getQueryParameter("code");
Log.d(TAG, "Received authorization code: " + authorizationCode);
webview.setVisibility(View.GONE);
getUserAccessToken(authorizationCode, codeChallenge);
}
return false;
}
});
authenticateMAL(codeChallenge);
}
private void authenticateMAL(String codeChallenge) {
Log.d(TAG, "Code challenge (" + codeChallenge.length() + "): " + codeChallenge);
String loginUrl = OAUTH_BASE_URL + "authorize" +
"?response_type=code" +
"&redirect_uri=" + REDIRECT_URL +
"&client_id=" + CLIENT_ID +
"&code_challenge=" + codeChallenge;
Log.d(TAG, "Login url: " + loginUrl);
webview.loadUrl(loginUrl);
}
As far as I can see, this works well. I am getting the authorizationCode as expected.
Step 2: Getting the user access token & refresh token
Here, I am using Mal4J for the next authentication step:
private void getUserAccessToken(String authorizationCode, String codeChallenge) {
Single.fromCallable(() -> {
MyAnimeListAuthenticator authenticator = new MyAnimeListAuthenticator(
CLIENT_ID, null, authorizationCode, codeChallenge);
return authenticator.getAccessToken();
})
.subscribeOn(Schedulers.io())
.doOnError(throwable -> {
Log.e(TAG, "Error while retrieving token!", throwable);
})
.onErrorComplete()
.subscribe(token -> {
Log.d(TAG, "--> access token: " + token.getToken());
Log.d(TAG, "--> refresh token: " + token.getRefreshToken());
});
}
Unfortunately, this results in the following error:
E/LoginActivity: Error while retrieving token!
com.kttdevelopment.mal4j.HttpException: Server returned code 400 from 'https://myanimelist.net/v1/oauth2/token':
at com.kttdevelopment.mal4j.MyAnimeListAuthenticator.parseToken(MyAnimeListAuthenticator.java:505)
at com.kttdevelopment.mal4j.MyAnimeListAuthenticator.<init>(MyAnimeListAuthenticator.java:139)
at florian.baierl.daily_anime_news.ui.LoginActivity.lambda$getUserAccessToken$0(LoginActivity.java:99)
at florian.baierl.daily_anime_news.ui.-$$Lambda$LoginActivity$-bBBIb9OKRzdaFNsFkQdJSeVW74.call(Unknown Source:4)
at io.reactivex.rxjava3.internal.operators.single.SingleFromCallable.subscribeActual(SingleFromCallable.java:43)
at io.reactivex.rxjava3.core.Single.subscribe(Single.java:4813)
at io.reactivex.rxjava3.internal.operators.single.SingleSubscribeOn$SubscribeOnObserver.run(SingleSubscribeOn.java:89)
at io.reactivex.rxjava3.core.Scheduler$DisposeTask.run(Scheduler.java:614)
at io.reactivex.rxjava3.internal.schedulers.ScheduledRunnable.run(ScheduledRunnable.java:65)
at io.reactivex.rxjava3.internal.schedulers.ScheduledRunnable.call(ScheduledRunnable.java:56)
at java.util.concurrent.FutureTask.run(FutureTask.java:266)
at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:301)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1167)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641)
at java.lang.Thread.run(Thread.java:923)
Any ideas as to why that may happen? Am I missing some Android specific stuff for Oauth2? As far as I can see, I am correctly retrieving the auth code from step 1. After that, my code seems very straight-forward, so I fail to see where the error could be. Any hints are greatly appreciated!
Edit:
This is how the request looks like (from the android studio profile view):
and here is the reply:
Edit 2:
Hard coding the code challenge/verifier to 128 times 'A' (AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA) doesn't change the behavior either:
When you include the redirect_uri in the authorization request you also need to include it in the /token request. Maybe it't that.
I'm trying to implement SafetyNet in an Android app but am running into this issue:
The JWS that I receive show the nonce as a particular value, but it is different than the one I passed in here:
Task<SafetyNetApi.AttestationResponse> task = client.attest(nonce.getBytes(), apiKey);
task.addOnSuccessListener(this, new OnSuccessListener<SafetyNetApi.AttestationResponse>() {
#Override
public void onSuccess(SafetyNetApi.AttestationResponse attestationResponse) {
handleJWS(attestationResponse.getJwsResult());
}
}).addOnFailureListener(this, new OnFailureListener() {
#Override
public void onFailure(#NonNull Exception e) {
if (BuildConfig.DEBUG)
Log.d("cts", "fail " + e.toString());
}
});
I use nonce.getBytes() as a parameter, as the nonce generated from the server is a String.
I've read the documentation over and over again, but can't figure out why the nonce value I receive as part of the JWS doesn't match the value I put in the attest() method. If this is expected, why is this so and how can I get the server to expect this value in order to verify it?
Any ideas?
Thanks!
I had the same issue and just figured out what was wrong: the nonce, apkDigestSha256 and the values of apkCertificateDigestSha256 are Base64 encoded! See https://github.com/googlesamples/android-play-safetynet/blob/master/server/java/src/main/java/AttestationStatement.java
In order to compare your nonce with the one from the JWS you have to do:
Arrays.equals(yourNonce, Base64.decode(attestationStatement.nonce(), Base64.DEFAULT))
I've been trying to build some functionality into my app too allow user-generated data (EEG recordings) to be sent to a central BigQuery database.
I've never done any networking code in Java before, so I shied away from doing the POST or REST-based strategies recommended here. The BigQuery Java client library seemed to be exactly what I needed, though I was completely confused why it wouldn't officially support Android.
Still, I came across this example Android app (from Google no less) that promised to do exactly what I wanted with the BigQuery Client library. I incorporated it into my app as follows:
// .... in an AsyncTask
#Override
protected String doInBackground(String... params) {
String CSV_CONTENT = params[0];
try {
AssetManager am = MainApplication.getInstance().getAssets();
InputStream isCredentialsFile = am.open(CREDENTIALS_FILE);
BigQuery bigquery = BigQueryOptions.builder()
.authCredentials(AuthCredentials.createForJson(isCredentialsFile))
.projectId( PROJECT_ID )
.build().service();
TableId tableId = TableId.of(DATASET,TABLE);
Table table = bigquery.getTable(tableId);
int num = 0;
Log.d("Main", "Sending CSV: ");
WriteChannelConfiguration configuration = WriteChannelConfiguration.builder(tableId)
.formatOptions(FormatOptions.csv())
.build();
try (WriteChannel channel = bigquery.writer(configuration)) {
num = channel.write(ByteBuffer.wrap(CSV_CONTENT.getBytes(StandardCharsets.UTF_8)));
} catch (Exception e) {
Log.d("Main", e.toString());
}
Log.d("Main", "Loading " + Integer.toString(num) + " bytes into table " + tableId);
} catch (Exception e) {
Log.d("Main", "Exception: " + e.toString());
}
return "Done";
}
This runs without any errors and fires off an API call that is detected by Google Cloud Storage. However, it returns error 200 (job was cancelled) every time. I don't understand how this could be since I'm not doing anything in the code to cancel the request and I don't see how the async task I put the call in could be cancelled.
Was this just a bad example app I copied and a bad usage of the BigQuery Client? If so, what's the best way to send data to BigQuery from Android?
Code Returns Exception Invalid Paramters
I need to know what are the invalid params ?
Is Application ARN string value from where to get it or it is a static value .
when to send device if returned from GCM ?
CreatePlatformEndpointRequest cpeReq =
new CreatePlatformEndpointRequest();
cpeReq.setCustomUserData("");
cpeReq.setToken(this.mToken);
cpeReq.setPlatformApplicationArn(retrieveEndpointArn());
client.setRegion(Region.getRegion(Regions.DEFAULT_REGION));
CreatePlatformEndpointResult cpeRes = client
.createPlatformEndpoint(cpeReq);
endpointArn = cpeRes.getEndpointArn();
} catch (InvalidParameterException ipe) {
String message = ipe.getErrorMessage();
System.out.println("Exception message: " + message);
Pattern p = Pattern
.compile(".*Endpoint (arn:aws:sns[^ ]+) already exists " +
"with the same Token.*");
Matcher m = p.matcher(message);
if (m.matches()) {
// the endpoint already exists for this token, but with
// additional custom data that
// CreateEndpoint doesn't want to overwrite. Just use the
// existing endpoint.
endpointArn = m.group(1);
} else {
// rethrow exception, the input is actually bad
throw ipe;
}
}
I need to upload a bitmap to Amazon S3. Below is the code I have so far, built after going through the docs and sample code.
public class AmazonS3Test {
private static final String TAG = "MyApp.AmazonS3Stuff";
private static AmazonS3 mS3 = null;
private static final String mS3BucketName = "bucketname";
private static BasicAWSCredentials mCredentials = new BasicAWSCredentials(".....", "....");
private static void uploadImageToAmazonS3(String key, File file) {
PutObjectRequest request = new PutObjectRequest(mS3BucketName, key, file);
try {
PutObjectResult result = getS3Instance().putObject(request);
} catch (AmazonClientException e) {
Log.e(TAG, "Amazon exception uploading the image to Amazon S3 " + key + " " + mS3BucketName, e);
} catch (Exception e) {
Log.e(TAG, "Exception uploading the image to Amazon S3 " + key + " " + mS3BucketName, e);
}
// TODO Handle result
}
private static AmazonS3 getS3Instance() {
if (mS3 == null) {
mS3 = new AmazonS3Client(mCredentials); // <---- Exception here
}
return mS3;
}
}
Stack trace:
Caused by: java.lang.NoSuchMethodError: org.apache.commons.httpclient.params.i.a
at com.amazonaws.http.HttpClient.<init>(Unknown Source)
at com.amazonaws.AmazonWebServiceClient.<init>(Unknown Source)
at com.amazonaws.services.s3.AmazonS3Client.<init>(Unknown Source)
at com.amazonaws.services.s3.AmazonS3Client.<init>(Unknown Source)
at com.addapps.taxiapp.utils.Utils.getS3Instance(AmazonS3Test.java)
I've little idea what that exception means, or why I cannot get an instance of AmazonS3.
I'm really struggling with S3, so if anyone can point me to any good resources I'd really appreciate it.
This exception was caused by my not having all the required AWS libraries in the project. AWS has a lot of libraries in different forms, and although not clear which are required, it won't work until the right ones are present.
Internally, the Amazon Web Service Client is using Apache commons HttpClient. From the stacktrace, it appears as if you have a library conflict. Check to see if you are using HttpClient somewhere else in your project. Most likely, you are using a different version than the Amazon Web Service Client is expecting.