parent
70b50c8989
commit
931b6608d5
|
@ -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.
|
|
@ -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.
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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.
|
||||
|
|
Loading…
Reference in New Issue