This commit is contained in:
Michele Balistreri 2018-11-15 20:16:18 +03:00
parent 70b50c8989
commit 931b6608d5
5 changed files with 275 additions and 5 deletions

8
README.md Normal file
View File

@ -0,0 +1,8 @@
# Hardwallet Android SDK
This SDK simplifies integration with the [Status Keycard](https://github.com/status-im/hardware-wallet) in Android
applications. In this SDK you find both the classes needed for generic communication with SmartCards as well as classes
specifically addressing the Keycard.
To get started, check the file demo/src/main/java/im/status/hardwallet_lite_android/app/MainActivity.java which a simple
demo application showing how the SDK works and what you can do with it.

View File

@ -8,10 +8,7 @@ import im.status.hardwallet_lite_android.demo.R;
import im.status.hardwallet_lite_android.io.CardChannel;
import im.status.hardwallet_lite_android.io.CardManager;
import im.status.hardwallet_lite_android.io.OnCardConnectedListener;
import im.status.hardwallet_lite_android.wallet.ApplicationInfo;
import im.status.hardwallet_lite_android.wallet.ApplicationStatus;
import im.status.hardwallet_lite_android.wallet.Pairing;
import im.status.hardwallet_lite_android.wallet.WalletAppletCommandSet;
import im.status.hardwallet_lite_android.wallet.*;
import org.spongycastle.util.encoders.Hex;
public class MainActivity extends AppCompatActivity {
@ -62,7 +59,7 @@ public class MainActivity extends AppCompatActivity {
cmdSet.autoPair("WalletAppletTest");
Pairing pairing = cmdSet.getPairing();
// Never log the pairing key in a real application
// Never log the pairing key in a real application!
Log.i(TAG, "Pairing with card is done.");
Log.i(TAG, "Pairing index: " + pairing.getPairingIndex());
Log.i(TAG, "Pairing key: " + Hex.toHexString(pairing.getPairingKey()));
@ -84,6 +81,27 @@ public class MainActivity extends AppCompatActivity {
Log.i(TAG, "Pin Verified.");
// If the card has no keys, we generate a new set. Keys can also be loaded on the card starting from a binary
// seed generated from a mnemonic phrase. The card can also generate mnemonics.
if (!status.hasMasterKey()) {
cmdSet.generateKey();
}
// Key derivation is needed to select the desired key. The derived key remains current until a new derive
// command is sent (it is not lost on power loss). With GET STATUS one can retrieve the current path.
cmdSet.deriveKey("m/44'/0'/0'/0/0").checkOK();
Log.i(TAG, "Derived m/44'/0'/0'/0/0");
byte[] hash = "thiscouldbeahashintheorysoitisok".getBytes();
RecoverableSignature signature = new RecoverableSignature(hash, cmdSet.sign(hash).checkOK().getData());
Log.i(TAG, "Signed hash: " + Hex.toHexString(hash));
Log.i(TAG, "Recovery ID: " + signature.getRecId());
Log.i(TAG, "R: " + Hex.toHexString(signature.getR()));
Log.i(TAG, "S: " + Hex.toHexString(signature.getS()));
// Cleanup, in a real application you would not unpair and instead keep the pairing key for successive interactions.
// We also remove all other pairings so that we do not fill all slots with failing runs. Again in real application
// this would be a very bad idea to do.

View File

@ -0,0 +1,102 @@
package im.status.hardwallet_lite_android.wallet;
import java.util.StringTokenizer;
/**
* Keypath object to be used with the WalletAppletCommandSet
*/
public class KeyPath {
private int source;
private byte[] data;
/**
* Parses a keypath into a byte array and source parameter to be used with the WalletAppletCommandSet object.
*
* A valid string is composed of a minimum of one and a maximum of 11 components separated by "/".
*
* The first component can be either "m", indicating the master key, "..", indicating the parent of the current key,
* or "." indicating the current key. It can also be omitted, in which case it is considered the same as being ".".
*
* All other components are positive integers fitting in 31 bit, eventually suffixed by an apostrophe (') sign,
* which indicates an hardened key.
*
* An example of a valid path is "m/44'/0'/0'/0/0"
*
*
* @param keypath the keypath as a string
*/
public KeyPath(String keypath) {
data = new byte[40];
StringTokenizer tokenizer = new StringTokenizer(keypath, "/");
String sourceOrFirstElement = tokenizer.nextToken();
switch(sourceOrFirstElement) {
case "m":
source = WalletAppletCommandSet.DERIVE_P1_SOURCE_MASTER;
break;
case "..":
source = WalletAppletCommandSet.DERIVE_P1_SOURCE_PARENT;
break;
case ".":
source = WalletAppletCommandSet.DERIVE_P1_SOURCE_CURRENT;
break;
default:
source = WalletAppletCommandSet.DERIVE_P1_SOURCE_CURRENT;
tokenizer = new StringTokenizer(keypath, "/"); // rewind
break;
}
int componentCount = tokenizer.countTokens();
if (componentCount > 10) {
throw new IllegalArgumentException("Too many components");
}
for (int i = 0; i < componentCount; i++) {
long component = parseComponent(tokenizer.nextToken());
writeComponent(component, i);
}
}
private long parseComponent(String num) {
long sign;
if (num.endsWith("'")) {
sign = 0x80000000L;
num = num.substring(0, (num.length() - 1));
} else {
sign = 0L;
}
if (num.startsWith("+") || num.startsWith("-")) {
throw new NumberFormatException("No sign allowed");
}
return (sign | Long.parseLong(num));
}
private void writeComponent(long component, int i) {
int off = (i*4);
data[off] = (byte)((component >> 24) & 0xff);
data[off + 1] = (byte)((component >> 16) & 0xff);
data[off + 2] = (byte)((component >> 8) & 0xff);
data[off + 3] = (byte)(component & 0xff);
}
/**
* The source of the derive command.
*
* @return the source of the derive command
*/
public int getSource() {
return source;
}
/**
* The byte encoded key path.
*
* @return byte encoded key path
*/
public byte[] getData() {
return data;
}
}

View File

@ -0,0 +1,130 @@
package im.status.hardwallet_lite_android.wallet;
import org.spongycastle.asn1.x9.X9ECParameters;
import org.spongycastle.asn1.x9.X9IntegerConverter;
import org.spongycastle.crypto.ec.CustomNamedCurves;
import org.spongycastle.crypto.params.ECDomainParameters;
import org.spongycastle.math.ec.ECAlgorithms;
import org.spongycastle.math.ec.ECPoint;
import org.spongycastle.math.ec.FixedPointUtil;
import org.spongycastle.math.ec.custom.sec.SecP256K1Curve;
import java.math.BigInteger;
import java.util.Arrays;
/**
* Signature with recoverable public key.
*/
public class RecoverableSignature {
private byte[] publicKey;
private int recId;
private byte[] r;
private byte[] s;
public static final byte TLV_SIGNATURE_TEMPLATE = (byte) 0xA0;
public static final byte TLV_ECDSA_TEMPLATE = (byte) 0x30;
private static final X9ECParameters CURVE_PARAMS = CustomNamedCurves.getByName("secp256k1");
private static final ECDomainParameters CURVE;
static {
FixedPointUtil.precompute(CURVE_PARAMS.getG(), 6);
CURVE = new ECDomainParameters(CURVE_PARAMS.getCurve(), CURVE_PARAMS.getG(), CURVE_PARAMS.getN(), CURVE_PARAMS.getH());
}
/**
* Parses a signature from the card and calculates the recovery ID.
*
* @param hash the message being signed
* @param tlvData the signature as returned from the card
*/
public RecoverableSignature(byte[] hash, byte[] tlvData) {
TinyBERTLV tlv = new TinyBERTLV(tlvData);
tlv.enterConstructed(TLV_SIGNATURE_TEMPLATE);
publicKey = tlv.readPrimitive(ApplicationInfo.TLV_PUB_KEY);
tlv.enterConstructed(TLV_ECDSA_TEMPLATE);
r = tlv.readPrimitive(TinyBERTLV.TLV_INT);
s = tlv.readPrimitive(TinyBERTLV.TLV_INT);
recId = -1;
for (int i = 0; i < 4; i++) {
byte[] candidate = recoverFromSignature(i, new BigInteger(1, hash), new BigInteger(1, r), new BigInteger(1, s));
if (Arrays.equals(candidate, publicKey)) {
recId = i;
break;
}
}
if (recId == -1) {
throw new IllegalArgumentException("Unrecoverable signature, cannot find recId");
}
}
/**
* The public key associated to this signature.
*
* @return the public key associated to this signature
*/
public byte[] getPublicKey() {
return publicKey;
}
/**
* The recovery ID
*
* @return recovery ID
*/
public int getRecId() {
return recId;
}
/**
* The R value.
*
* @return r
*/
public byte[] getR() {
return r;
}
/**
* The S value
* @return s
*/
public byte[] getS() {
return s;
}
private static byte[] recoverFromSignature(int recId, BigInteger e, BigInteger r, BigInteger s) {
BigInteger n = CURVE.getN();
BigInteger i = BigInteger.valueOf((long) recId / 2);
BigInteger x = r.add(i.multiply(n));
BigInteger prime = SecP256K1Curve.q;
if (x.compareTo(prime) >= 0) {
return null;
}
ECPoint R = decompressKey(x, (recId & 1) == 1);
if (!R.multiply(n).isInfinity()) {
return null;
}
BigInteger eInv = BigInteger.ZERO.subtract(e).mod(n);
BigInteger rInv = r.modInverse(n);
BigInteger srInv = rInv.multiply(s).mod(n);
BigInteger eInvrInv = rInv.multiply(eInv).mod(n);
ECPoint q = ECAlgorithms.sumOfTwoMultiplies(CURVE.getG(), eInvrInv, R, srInv);
return q.getEncoded(false);
}
private static ECPoint decompressKey(BigInteger xBN, boolean yBit) {
X9IntegerConverter x9 = new X9IntegerConverter();
byte[] compEnc = x9.integerToBytes(xBN, 1 + x9.getByteLength(CURVE.getCurve()));
compEnc[0] = (byte)(yBit ? 0x03 : 0x02);
return CURVE.getCurve().decodePoint(compEnc);
}
}

View File

@ -469,6 +469,18 @@ public class WalletAppletCommandSet {
return secureChannel.transmit(apduChannel, sign);
}
/**
* Sends a DERIVE KEY APDU with the given key path.
*
* @param keypath the string key path
* @return the raw card response
* @throws IOException communication error
*/
public APDUResponse deriveKey(String keypath) throws IOException {
KeyPath path = new KeyPath(keypath);
return deriveKey(path.getData(), path.getSource());
}
/**
* Sends a DERIVE KEY APDU. The data is encrypted and sent as-is. The P1 is forced to 0, meaning that the derivation
* starts from the master key.