Skip to content

Instantly share code, notes, and snippets.

@PaddeK
Created July 12, 2015 03:22
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save PaddeK/6ce4b7210993f7995d77 to your computer and use it in GitHub Desktop.
Save PaddeK/6ce4b7210993f7995d77 to your computer and use it in GitHub Desktop.
Verify Signatures within the new Nymi Android SDK 3.0.1 BETA
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')
}
<?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>
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 "";
}
}
}
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@id/popup_menu_notify_positive"
android:title="notify positive"/>
<item
android:id="@id/popup_menu_notify_negative"
android:title="notify negative"/>
<item
android:id="@id/popup_menu_get_random"
android:title="get random"/>
<item
android:id="@id/popup_menu_sign"
android:title="sign"/>
<item
android:id="@id/popup_menu_verify"
android:title="verify"/>
</menu>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment