Created
July 12, 2015 03:22
-
-
Save PaddeK/6ce4b7210993f7995d77 to your computer and use it in GitHub Desktop.
Verify Signatures within the new Nymi Android SDK 3.0.1 BETA
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
apply plugin: 'com.android.application' | |
android { | |
compileSdkVersion 22 | |
buildToolsVersion "22.0.1" | |
lintOptions { | |
abortOnError false | |
} | |
defaultConfig { | |
applicationId "com.nymi.nymireferenceapp" | |
minSdkVersion 18 | |
targetSdkVersion 22 | |
versionCode 1 | |
versionName "1.0" | |
} | |
buildTypes { | |
release { | |
minifyEnabled false | |
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' | |
} | |
} | |
} | |
task write_buildhost { | |
ext.versionfile = new File(file("src/main/res/raw").absolutePath + "/buildhost"); | |
versionfile.text = InetAddress.getLocalHost().getHostAddress() + "\n"; | |
} | |
task compile << { | |
dependsOn write_buildhost | |
} | |
dependencies { | |
compile fileTree(dir: 'libs', include: ['*.jar']) | |
compile 'com.madgag.spongycastle:core:1.52.0.0' | |
compile 'com.madgag.spongycastle:prov:1.52.0.0' | |
compile 'com.android.support:appcompat-v7:22.2.0' | |
compile(name: 'nymi-api-nymulator', ext:'aar') | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?xml version="1.0" encoding="utf-8"?> | |
<resources> | |
<item name="layout_main_led_panel" type="id"/> | |
<item name="layout_main_led0" type="id"/> | |
<item name="layout_main_led1" type="id"/> | |
<item name="layout_main_led2" type="id"/> | |
<item name="layout_main_led3" type="id"/> | |
<item name="layout_main_led4" type="id"/> | |
<item name="layout_main_button_accept" type="id"/> | |
<item name="layout_main_button_decline" type="id"/> | |
<item name="layout_main_provision_list" type="id"/> | |
<item name="layout_main_divider_1" type="id"/> | |
<item name="layout_main_divider_2" type="id"/> | |
<item name="layout_main_agreement_label" type="id"/> | |
<item name="layout_main_provisions_label" type="id"/> | |
<item name="layout_provision_row_provision" type="id"/> | |
<item name="layout_provision_row_action_button" type="id"/> | |
<item name="popup_menu_notify_positive" type="id"/> | |
<item name="popup_menu_notify_negative" type="id"/> | |
<item name="popup_menu_get_random" type="id"/> | |
<item name="popup_menu_sign" type="id"/> | |
<item name="popup_menu_verify" type="id"/> | |
</resources> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package com.nymi.nymireferenceapp; | |
import android.os.Bundle; | |
import android.support.v7.app.ActionBarActivity; | |
import android.support.v7.widget.PopupMenu; | |
import android.view.MenuItem; | |
import android.view.View; | |
import android.widget.AdapterView; | |
import android.widget.Button; | |
import android.widget.ListView; | |
import android.widget.RadioButton; | |
import android.widget.Toast; | |
import com.nymi.api.NymiAdapter; | |
import com.nymi.api.NymiDevice; | |
import com.nymi.api.NymiRandomNumber; | |
import org.spongycastle.asn1.ASN1EncodableVector; | |
import org.spongycastle.asn1.ASN1Integer; | |
import org.spongycastle.asn1.DERSequence; | |
import org.spongycastle.jce.ECNamedCurveTable; | |
import org.spongycastle.jce.ECPointUtil; | |
import org.spongycastle.jce.provider.BouncyCastleProvider; | |
import org.spongycastle.jce.spec.ECNamedCurveParameterSpec; | |
import org.spongycastle.jce.spec.ECNamedCurveSpec; | |
import org.spongycastle.util.encoders.Hex; | |
import java.io.BufferedReader; | |
import java.io.IOException; | |
import java.io.InputStream; | |
import java.io.InputStreamReader; | |
import java.security.KeyFactory; | |
import java.security.MessageDigest; | |
import java.security.NoSuchAlgorithmException; | |
import java.security.PublicKey; | |
import java.security.Security; | |
import java.security.Signature; | |
import java.security.spec.ECPoint; | |
import java.security.spec.ECPublicKeySpec; | |
import java.security.spec.InvalidKeySpecException; | |
import java.util.BitSet; | |
public class MainActivity extends ActionBarActivity { | |
static { | |
Security.insertProviderAt(new BouncyCastleProvider(), 1); | |
} | |
private static final String NEA_NAME = "AndroidExampleNEA"; // must be <= 18 characters | |
private static final String MESSAGE_TO_SIGN = "Message to be signed"; | |
private static final int LEDS_NUMBER = 5; | |
private NymiAdapter mNymiAdapter; | |
private AdapterProvisions mAdapterProvisions; | |
private ListView mListViewProvisions; | |
private RadioButton mLeds[]; | |
private Button mButtonAccept; | |
private Button mButtonDecline; | |
private String lastSignature; | |
private String lastVerifyingKey; | |
protected byte[] convertToPublicKey(String verifyingKey) { | |
return Hex.decode("04" + verifyingKey); | |
} | |
protected byte[] convertToHashed(String plaintext) { | |
MessageDigest md; | |
byte[] result = {}; | |
try { | |
md = MessageDigest.getInstance("SHA-256"); | |
md.update(plaintext.getBytes("UTF-8")); | |
result = md.digest(); | |
} catch (Exception e) { | |
e.printStackTrace(); | |
} | |
return result; | |
} | |
protected byte[] convertToDER(String signature) throws IOException { | |
int len = signature.length(); | |
if (len % 2 != 0) { | |
throw new IllegalArgumentException("Provided signature is not a valid Hex String"); | |
} | |
String r = signature.substring(0, len >> 1); | |
String s = signature.substring(len >> 1); | |
ASN1Integer rr = new ASN1Integer(Hex.decode(r.charAt(0) > '7' ? "00" + r : r)); | |
ASN1Integer ss = new ASN1Integer(Hex.decode(s.charAt(0) > '7' ? "00" + s : s)); | |
ASN1EncodableVector sig = new ASN1EncodableVector(); | |
sig.add(rr); | |
sig.add(ss); | |
return new DERSequence(sig).getEncoded(); | |
} | |
private PublicKey getPublicKeyFromBytes(byte[] pubKey) throws NoSuchAlgorithmException, InvalidKeySpecException { | |
ECNamedCurveParameterSpec spec = ECNamedCurveTable.getParameterSpec("P-256"); | |
KeyFactory kf = KeyFactory.getInstance("ECDSA", new BouncyCastleProvider()); | |
ECNamedCurveSpec params = new ECNamedCurveSpec("P-256", spec.getCurve(), spec.getG(), spec.getN()); | |
ECPoint point = ECPointUtil.decodePoint(params.getCurve(), pubKey); | |
ECPublicKeySpec pubKeySpec = new ECPublicKeySpec(point, params); | |
return kf.generatePublic(pubKeySpec); | |
} | |
protected boolean verify(String plainText, String signature, String verifyingKey) { | |
try { | |
byte[] pubBytes = convertToPublicKey(verifyingKey); | |
byte[] signBytes = convertToDER(signature); | |
byte[] textBytes = convertToHashed(plainText); | |
PublicKey pubKey = getPublicKeyFromBytes(pubBytes); | |
Signature ecdsaVerify = Signature.getInstance("SHA256withECDSA"); | |
ecdsaVerify.initVerify(pubKey); | |
ecdsaVerify.update(textBytes); | |
return ecdsaVerify.verify(signBytes); | |
} catch (Exception e) { | |
e.printStackTrace(); | |
} | |
return false; | |
} | |
@Override | |
protected void onCreate(Bundle savedInstanceState) { | |
super.onCreate(savedInstanceState); | |
setContentView(R.layout.activity_main); | |
mNymiAdapter = NymiAdapter.getInstance(); // get singleton | |
String nymulatorHost = loadBuildhostFromResources(); | |
// in typical dev environments, the nymulator will be run on the | |
// same machine as your build machine, so this is a sensible default. | |
// If that's not the case for you, update this host field, e.g. | |
// nymulatorHost = "10.0.1.11" | |
mNymiAdapter.setNymulator(nymulatorHost); | |
// Initialize the NymiAdapter. We'll get a callback sometime in the future | |
// when initialization can finish. Initialization can fail, but that's only if | |
// you've specified a nymulator host that the backend is unable to talk to. | |
mNymiAdapter.init(this, NEA_NAME, new NymiAdapter.NymiInitCallback() { | |
@Override | |
public void onNymiInitResult(int status) { | |
if (status == NymiAdapter.NymiInitCallback.INIT_SUCCESS) { | |
// All callbacks from the NymiAdapter run on the UI thread, | |
// so you may safely update your UI (and the usual caveats apply) | |
Toast.makeText(MainActivity.this, "Initialized", Toast.LENGTH_SHORT).show(); | |
mAdapterProvisions.setDevices(NymiAdapter.getInstance().getDevices()); | |
// on success we immediately start provisioning. | |
// Starting provisioning in the callback avoids having to poll | |
// for init to continue before kicking off the method. | |
startProvision(); | |
} else { | |
// The only failure is if we couldn't contact a nymulator | |
// at the nymulator host set on line 48. | |
Toast.makeText(MainActivity.this, "Failed to initialize", Toast.LENGTH_SHORT).show(); | |
} | |
} | |
}); | |
// Adapter for displaying all the provisions. | |
mAdapterProvisions = new AdapterProvisions(this); | |
// Here we set up our UI for displaying agreement patterns. | |
// Confirming the LED hash against what the user sees on their band | |
// is a crucial step during provisioning. Without this, you may | |
// unintentionally provision the wrong band and are susceptible to | |
// man-in-the-middle attacks | |
mLeds = new RadioButton[LEDS_NUMBER]; | |
// UI to display patterns | |
mLeds[0] = (RadioButton) findViewById(R.id.layout_main_led0); | |
mLeds[1] = (RadioButton) findViewById(R.id.layout_main_led1); | |
mLeds[2] = (RadioButton) findViewById(R.id.layout_main_led2); | |
mLeds[3] = (RadioButton) findViewById(R.id.layout_main_led3); | |
mLeds[4] = (RadioButton) findViewById(R.id.layout_main_led4); | |
// Buttons to accept User's communication of whether to accept or | |
// reject the candidate provision. | |
mButtonAccept = (Button) findViewById(R.id.layout_main_button_accept); | |
mButtonDecline = (Button) findViewById(R.id.layout_main_button_decline); | |
mButtonAccept.setOnClickListener(new View.OnClickListener() { | |
@Override | |
public void onClick(View view) { | |
BitSet bitSet = new BitSet(LEDS_NUMBER); | |
bitSet.clear(); | |
for (int i = 0; i < LEDS_NUMBER; i++) { | |
if (mLeds[i].isChecked()) { | |
bitSet.set(i); | |
} | |
mLeds[i].setChecked(false); | |
mLeds[i].setEnabled(false); | |
} | |
// Calling setPattern is your application's way of saying | |
// "Yes, I really want to provision with this device." | |
// After setPattern, you'll get a callback with a NymiDevice | |
// instance with which your application can interact. | |
mNymiAdapter.setPattern(bitSet); | |
mButtonAccept.setEnabled(false); | |
mButtonDecline.setEnabled(false); | |
} | |
}); | |
mButtonDecline.setOnClickListener(new View.OnClickListener() { | |
@Override | |
public void onClick(View view) { | |
for (int i = 0; i < LEDS_NUMBER; i++) { | |
mLeds[i].setChecked(false); | |
mLeds[i].setEnabled(false); | |
} | |
mButtonAccept.setEnabled(false); | |
mButtonDecline.setEnabled(false); | |
Toast.makeText(MainActivity.this, "Device declined", Toast.LENGTH_SHORT).show(); | |
} | |
}); | |
// Of course the main thing your application will want to do is interact | |
// with NymiDevice objects themselves. Here are the main facilities of | |
// the UI. All operations have the model of an operation being | |
// dispatched with a callback, and that callback eventually being | |
// called. Note that operations can fail if the target band is absent | |
// or unavailable (e.g. busy communicating with another NEA) | |
mListViewProvisions = (ListView) findViewById(R.id.layout_main_provision_list); | |
mListViewProvisions.setOnItemClickListener(new AdapterView.OnItemClickListener() { | |
@Override | |
public void onItemClick(AdapterView<?> parent, View view, final int position, long id) { | |
PopupMenu popup = new PopupMenu(MainActivity.this, view); | |
popup.getMenuInflater().inflate(R.menu.popup_menu, popup.getMenu()); | |
popup.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() { | |
public boolean onMenuItemClick(MenuItem item) { | |
switch (item.getItemId()) { | |
case R.id.popup_menu_notify_positive: | |
((NymiDevice) mAdapterProvisions.getItem(position)).sendNotification(NymiDevice.NymiDeviceNotification.POSITIVE, new NymiDevice.NymiNotificationCallback() { | |
@Override | |
public void onNymiNotificationResult(int status, NymiDevice.NymiDeviceNotification nymiDeviceNotification) { | |
if (status == NymiDevice.NymiNotificationCallback.NOTIFICATION_SUCCESS) { | |
Toast.makeText(MainActivity.this, nymiDeviceNotification.toString() + " notification completed", Toast.LENGTH_SHORT).show(); | |
} else { | |
Toast.makeText(MainActivity.this, "Notification failed", Toast.LENGTH_SHORT).show(); | |
} | |
} | |
}); | |
break; | |
case R.id.popup_menu_notify_negative: | |
((NymiDevice) mAdapterProvisions.getItem(position)).sendNotification(NymiDevice.NymiDeviceNotification.NEGATIVE, new NymiDevice.NymiNotificationCallback() { | |
@Override | |
public void onNymiNotificationResult(int status, NymiDevice.NymiDeviceNotification nymiDeviceNotification) { | |
if (status == NymiDevice.NymiNotificationCallback.NOTIFICATION_SUCCESS) { | |
Toast.makeText(MainActivity.this, nymiDeviceNotification.toString() + " notification completed", Toast.LENGTH_SHORT).show(); | |
} else { | |
Toast.makeText(MainActivity.this, "Notification failed", Toast.LENGTH_SHORT).show(); | |
} | |
} | |
}); | |
break; | |
case R.id.popup_menu_get_random: | |
((NymiDevice) mAdapterProvisions.getItem(position)).getRandom(new NymiDevice.NymiRandomCallback() { | |
@Override | |
public void onNymiRandomResult(int status, NymiRandomNumber nymiRandomNumber) { | |
if (status == NymiDevice.NymiRandomCallback.RANDOM_SUCCESS) { | |
// Of course a real application will want to make use of this value otherwise, | |
// but this is to demonstrate flow. | |
Toast.makeText(MainActivity.this, "Obtained random: " + nymiRandomNumber.toString(), Toast.LENGTH_SHORT).show(); | |
} else { | |
Toast.makeText(MainActivity.this, "Random failed", Toast.LENGTH_SHORT).show(); | |
} | |
} | |
}); | |
break; | |
case R.id.popup_menu_sign: | |
final NymiDevice device = (NymiDevice) mAdapterProvisions.getItem(position); | |
if (device != null && device.getKeys() != null && !device.getKeys().isEmpty()) { | |
device.sign(MESSAGE_TO_SIGN, device.getKeys().get(0), new NymiDevice.NymiSignCallback() { | |
@Override | |
public void onMessageSigned(int status, String signature) { | |
if (status == NymiDevice.NymiSignCallback.SIGN_LOCAL_SUCCESS) { | |
// Of course your code will want to make use of this value otherwise. | |
// This code intends to demonstrate flow. | |
lastSignature = signature; | |
lastVerifyingKey = device.getKeys().get(0).getVerifyingKey(); | |
Toast.makeText(MainActivity.this, "Sign (on a dummy message) returned: " + signature, Toast.LENGTH_SHORT).show(); | |
} else { | |
Toast.makeText(MainActivity.this, "Sign failed", Toast.LENGTH_SHORT).show(); | |
} | |
} | |
}); | |
} else { | |
Toast.makeText(MainActivity.this, "Error retrieving keys", Toast.LENGTH_SHORT).show(); | |
} | |
break; | |
case R.id.popup_menu_verify: | |
if (lastSignature == null || lastVerifyingKey == null) { | |
Toast.makeText(MainActivity.this, "Use Sign first", Toast.LENGTH_SHORT).show(); | |
} else { | |
Toast.makeText(MainActivity.this, "Verify result: " + verify(MESSAGE_TO_SIGN, lastSignature, lastVerifyingKey), Toast.LENGTH_LONG).show(); | |
} | |
break; | |
default: | |
break; | |
} | |
return true; | |
} | |
}); | |
popup.show(); | |
} | |
}); | |
mListViewProvisions.setAdapter(mAdapterProvisions); | |
} | |
private void startProvision() { | |
boolean status = mNymiAdapter.startProvision(new NymiAdapter.NymiProvisionCallback() { | |
@Override | |
public void onDeviceProvisioned(int status, NymiDevice nymiDevice, BitSet bitSet) { | |
if (status == NymiAdapter.NymiProvisionCallback.PROVISION_SUCCESS) { | |
mAdapterProvisions.addDevice(nymiDevice); | |
} else { | |
// Provisioning can fail due to connectivity problems. | |
// Unfortunately, your applications only recovery is to | |
// start the provisioning process over. You'll need to | |
// instruct the user to put their band back into provisioning mode. | |
Toast.makeText(MainActivity.this, "Error completing provision.", Toast.LENGTH_SHORT).show(); | |
} | |
} | |
@Override | |
public void onNymiAgreement(BitSet bitSet) { | |
for (int i = 0; i < LEDS_NUMBER; i++) { | |
mLeds[i].setChecked(bitSet.get(i)); | |
mLeds[i].setEnabled(true); | |
} | |
mButtonAccept.setEnabled(true); | |
mButtonDecline.setEnabled(true); | |
} | |
}); | |
if (status) { | |
Toast.makeText(MainActivity.this, "Provision started", Toast.LENGTH_SHORT).show(); | |
} else { | |
Toast.makeText(MainActivity.this, "Error starting provision", Toast.LENGTH_SHORT).show(); | |
} | |
} | |
// This build is configured so that the IP address of the host building | |
// the apk is included among the resources and retrieved here. This is a | |
// commmon trick, as most app development needs some server to talk to, | |
// and in development settings, that server host will typically be your | |
// build machine. | |
private String loadBuildhostFromResources() { | |
try { | |
InputStream buildhost_stream = getResources().openRawResource(R.raw.buildhost); | |
return new BufferedReader(new InputStreamReader(buildhost_stream)).readLine(); | |
} catch (IOException e) { | |
// shouldn't happen. The build ensures this resource will be present. | |
return ""; | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment