|
35 | 35 | import java.io.InputStream; |
36 | 36 | import java.io.InputStreamReader; |
37 | 37 | import java.net.URL; |
| 38 | +import java.nio.charset.StandardCharsets; |
| 39 | +import java.util.Locale; |
38 | 40 |
|
39 | 41 | import javax.net.ssl.HttpsURLConnection; |
40 | 42 |
|
41 | 43 | // *** UNCOMMENT THE LINE BELOW FOR APPROOV *** |
42 | | -//import io.approov.service.httpsurlconn.ApproovService; |
| 44 | +import io.approov.service.httpsurlconn.ApproovService; |
43 | 45 |
|
44 | 46 | public class MainActivity extends Activity { |
45 | 47 | private static final String TAG = MainActivity.class.getSimpleName(); |
| 48 | + private static final int MESSAGE_SIGNING_BATCH_SIZE = 100; |
46 | 49 | private Activity activity; |
47 | 50 | private View statusView = null; |
48 | 51 | private ImageView statusImageView = null; |
@@ -115,6 +118,168 @@ private int readShapesResponse(HttpsURLConnection connection) { |
115 | 118 | return R.drawable.confused; |
116 | 119 | } |
117 | 120 |
|
| 121 | + private String readResponseBody(HttpsURLConnection connection) throws IOException { |
| 122 | + StringBuilder sb = new StringBuilder(); |
| 123 | + InputStream is = null; |
| 124 | + try { |
| 125 | + try { |
| 126 | + is = new BufferedInputStream(connection.getInputStream()); |
| 127 | + } catch (IOException e) { |
| 128 | + InputStream errorStream = connection.getErrorStream(); |
| 129 | + if (errorStream == null) |
| 130 | + throw e; |
| 131 | + is = new BufferedInputStream(errorStream); |
| 132 | + } |
| 133 | + BufferedReader br = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8)); |
| 134 | + String inputLine; |
| 135 | + while ((inputLine = br.readLine()) != null) |
| 136 | + sb.append(inputLine); |
| 137 | + return sb.toString(); |
| 138 | + } |
| 139 | + finally { |
| 140 | + if (is != null) { |
| 141 | + try { |
| 142 | + is.close(); |
| 143 | + } |
| 144 | + catch (IOException e) { |
| 145 | + Log.d(TAG, "Error closing Shapes response InputStream"); |
| 146 | + } |
| 147 | + } |
| 148 | + } |
| 149 | + } |
| 150 | + |
| 151 | + private static final class BatchResult { |
| 152 | + int validAccepted; |
| 153 | + int validRejected; |
| 154 | + int tamperedAccepted; |
| 155 | + int tamperedRejected; |
| 156 | + int networkErrors; |
| 157 | + int unexpectedResponses; |
| 158 | + } |
| 159 | + |
| 160 | + private String mutateSignatureHeader(String signatureHeader) { |
| 161 | + if (signatureHeader == null) |
| 162 | + return null; |
| 163 | + |
| 164 | + int firstColon = signatureHeader.indexOf(':'); |
| 165 | + int lastColon = signatureHeader.lastIndexOf(':'); |
| 166 | + if ((firstColon < 0) || (lastColon <= firstColon + 1)) |
| 167 | + return signatureHeader + "A"; |
| 168 | + |
| 169 | + char[] chars = signatureHeader.toCharArray(); |
| 170 | + for (int i = firstColon + 1; i < lastColon; i++) { |
| 171 | + char c = chars[i]; |
| 172 | + if (Character.isLetterOrDigit(c)) { |
| 173 | + chars[i] = (c == 'A') ? 'B' : 'A'; |
| 174 | + return new String(chars); |
| 175 | + } |
| 176 | + } |
| 177 | + chars[firstColon + 1] = (chars[firstColon + 1] == 'A') ? 'B' : 'A'; |
| 178 | + return new String(chars); |
| 179 | + } |
| 180 | + |
| 181 | + private boolean isAcceptedResponse(int responseCode, String responseBody) { |
| 182 | + return (responseCode >= 200) && (responseCode < 300) |
| 183 | + && (responseBody != null) |
| 184 | + && responseBody.contains("\"messageSigningResult\":\"VALID\""); |
| 185 | + } |
| 186 | + |
| 187 | + private boolean isRejectedResponse(int responseCode, String responseBody) { |
| 188 | + return (responseCode >= 400) |
| 189 | + || ((responseBody != null) && responseBody.contains("\"messageSigningResult\":\"INVALID\"")) |
| 190 | + || ((responseBody != null) && responseBody.contains("\"status\":\"FAIL\"")); |
| 191 | + } |
| 192 | + |
| 193 | + private void runMessageSigningBatchTest() { |
| 194 | + Log.d(TAG, "Starting installation message signing batch test with " + MESSAGE_SIGNING_BATCH_SIZE + " requests"); |
| 195 | + activity.runOnUiThread(new Runnable() { |
| 196 | + @Override |
| 197 | + public void run() { |
| 198 | + statusView.setVisibility(View.INVISIBLE); |
| 199 | + } |
| 200 | + }); |
| 201 | + |
| 202 | + AsyncTask.execute(new Runnable() { |
| 203 | + @Override |
| 204 | + public void run() { |
| 205 | + BatchResult batchResult = new BatchResult(); |
| 206 | + |
| 207 | + for (int i = 0; i < MESSAGE_SIGNING_BATCH_SIZE; i++) { |
| 208 | + boolean tamperSignature = (i % 2) == 1; |
| 209 | + HttpsURLConnection connection = null; |
| 210 | + String responseBody = null; |
| 211 | + try { |
| 212 | + URL url = new URL(getResources().getString(R.string.shapes_url)); |
| 213 | + connection = (HttpsURLConnection) url.openConnection(); |
| 214 | + connection.setRequestMethod("GET"); |
| 215 | + connection.setInstanceFollowRedirects(false); |
| 216 | + connection.addRequestProperty("Accept", "application/json"); |
| 217 | + connection.addRequestProperty("Api-Key", getResources().getString(R.string.shapes_api_key)); |
| 218 | + ApproovService.addApproov(connection); |
| 219 | + |
| 220 | + if (tamperSignature) { |
| 221 | + String signature = connection.getRequestProperty("Signature"); |
| 222 | + connection.setRequestProperty("Signature", mutateSignatureHeader(signature)); |
| 223 | + } |
| 224 | + |
| 225 | + connection.connect(); |
| 226 | + int responseCode = connection.getResponseCode(); |
| 227 | + responseBody = readResponseBody(connection); |
| 228 | + |
| 229 | + if (!tamperSignature) { |
| 230 | + if (isAcceptedResponse(responseCode, responseBody)) |
| 231 | + batchResult.validAccepted++; |
| 232 | + else if (isRejectedResponse(responseCode, responseBody)) |
| 233 | + batchResult.validRejected++; |
| 234 | + else |
| 235 | + batchResult.unexpectedResponses++; |
| 236 | + } else { |
| 237 | + if (isRejectedResponse(responseCode, responseBody)) |
| 238 | + batchResult.tamperedRejected++; |
| 239 | + else if (isAcceptedResponse(responseCode, responseBody)) |
| 240 | + batchResult.tamperedAccepted++; |
| 241 | + else |
| 242 | + batchResult.unexpectedResponses++; |
| 243 | + } |
| 244 | + |
| 245 | + Log.d(TAG, "Batch request " + (i + 1) + "/" + MESSAGE_SIGNING_BATCH_SIZE |
| 246 | + + " tampered=" + tamperSignature |
| 247 | + + " code=" + responseCode |
| 248 | + + " body=" + responseBody); |
| 249 | + } catch (Exception e) { |
| 250 | + batchResult.networkErrors++; |
| 251 | + Log.e(TAG, "Batch request " + (i + 1) + "/" + MESSAGE_SIGNING_BATCH_SIZE |
| 252 | + + " tampered=" + tamperSignature + " failed", e); |
| 253 | + } finally { |
| 254 | + if (connection != null) |
| 255 | + connection.disconnect(); |
| 256 | + } |
| 257 | + } |
| 258 | + |
| 259 | + final String summary = String.format( |
| 260 | + Locale.US, |
| 261 | + "Batch test complete (%d requests)\nvalid accepted: %d\nvalid rejected: %d\ntampered accepted: %d\ntampered rejected: %d\nnetwork errors: %d\nunexpected: %d", |
| 262 | + MESSAGE_SIGNING_BATCH_SIZE, |
| 263 | + batchResult.validAccepted, |
| 264 | + batchResult.validRejected, |
| 265 | + batchResult.tamperedAccepted, |
| 266 | + batchResult.tamperedRejected, |
| 267 | + batchResult.networkErrors, |
| 268 | + batchResult.unexpectedResponses |
| 269 | + ); |
| 270 | + |
| 271 | + activity.runOnUiThread(new Runnable() { |
| 272 | + @Override |
| 273 | + public void run() { |
| 274 | + statusImageView.setVisibility(View.GONE); |
| 275 | + statusTextView.setText(summary); |
| 276 | + statusView.setVisibility(View.VISIBLE); |
| 277 | + } |
| 278 | + }); |
| 279 | + } |
| 280 | + }); |
| 281 | + } |
| 282 | + |
118 | 283 | @Override |
119 | 284 | protected void onCreate(Bundle savedInstanceState) { |
120 | 285 | super.onCreate(savedInstanceState); |
@@ -185,62 +350,7 @@ public void run() { |
185 | 350 | shapesCheckButton.setOnClickListener(new View.OnClickListener() { |
186 | 351 | @Override |
187 | 352 | public void onClick(View view) { |
188 | | - // hide status |
189 | | - activity.runOnUiThread(new Runnable() { |
190 | | - @Override |
191 | | - public void run() { |
192 | | - statusView.setVisibility(View.INVISIBLE); |
193 | | - } |
194 | | - }); |
195 | | - |
196 | | - // run our HTTP request in a background thread to avoid blocking the UI thread |
197 | | - AsyncTask.execute(new Runnable() { |
198 | | - @Override |
199 | | - public void run() { |
200 | | - // fetch from the endpoint |
201 | | - int imgId = R.drawable.confused; |
202 | | - String msg; |
203 | | - HttpsURLConnection connection = null; |
204 | | - try { |
205 | | - URL url = new URL(getResources().getString(R.string.shapes_url)); |
206 | | - connection = (HttpsURLConnection) url.openConnection(); |
207 | | - connection.setRequestMethod("GET"); |
208 | | - // Keep the originally signed request target unchanged (redirects can invalidate signatures). |
209 | | - connection.setInstanceFollowRedirects(false); |
210 | | - connection.addRequestProperty("Api-Key", getResources().getString(R.string.shapes_api_key)); |
211 | | - |
212 | | - // *** UNCOMMENT THE LINE BELOW FOR APPROOV USING SECRETS PROTECTION *** |
213 | | - //ApproovService.addSubstitutionHeader("Api-Key", null); |
214 | | - |
215 | | - // *** UNCOMMENT THE LINE BELOW FOR APPROOV *** |
216 | | - //ApproovService.addApproov(connection); |
217 | | - |
218 | | - connection.connect(); |
219 | | - |
220 | | - msg = "Http status code " + connection.getResponseCode(); |
221 | | - if (connection.getResponseCode() == 200) |
222 | | - imgId = readShapesResponse(connection); |
223 | | - } catch (IOException e) { |
224 | | - Log.d(TAG, "Shapes call failed: " + e.toString()); |
225 | | - msg = "Shapes call failed: " + e.toString(); |
226 | | - } |
227 | | - if (connection != null) |
228 | | - connection.disconnect(); |
229 | | - |
230 | | - // display the result |
231 | | - final int finalImgId = imgId; |
232 | | - final String finalMsg = msg; |
233 | | - activity.runOnUiThread(new Runnable() { |
234 | | - @Override |
235 | | - public void run() { |
236 | | - statusImageView.setImageResource(finalImgId); |
237 | | - statusTextView.setText(finalMsg); |
238 | | - statusView.setVisibility(View.VISIBLE); |
239 | | - } |
240 | | - }); |
241 | | - |
242 | | - } |
243 | | - }); |
| 353 | + runMessageSigningBatchTest(); |
244 | 354 | } |
245 | 355 | }); |
246 | 356 | } |
|
0 commit comments