wallet import and export - coldcard and electrum

This commit is contained in:
Craig Raw
2020-04-25 11:39:29 +02:00
parent 98b1aa0b1d
commit 6d202f1522
26 changed files with 948 additions and 14 deletions
@@ -210,7 +210,7 @@ public class AppController implements Initializable {
Optional<String> walletName = dlg.showAndWait();
if(walletName.isPresent()) {
File walletFile = Storage.getStorage().getWalletFile(walletName.get());
Wallet wallet = new Wallet(PolicyType.SINGLE, ScriptType.P2WPKH);
Wallet wallet = new Wallet(walletName.get(), PolicyType.SINGLE, ScriptType.P2WPKH);
Tab tab = addWalletTab(walletFile, null, wallet);
tabs.getSelectionModel().select(tab);
}
@@ -0,0 +1,186 @@
package com.sparrowwallet.sparrow.external;
import com.google.common.io.CharStreams;
import com.google.gson.Gson;
import com.sparrowwallet.drongo.ExtendedPublicKey;
import com.sparrowwallet.drongo.KeyDerivation;
import com.sparrowwallet.drongo.Utils;
import com.sparrowwallet.drongo.policy.Policy;
import com.sparrowwallet.drongo.policy.PolicyType;
import com.sparrowwallet.drongo.protocol.ScriptType;
import com.sparrowwallet.drongo.wallet.Keystore;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.storage.Storage;
import java.io.*;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
public class ColdcardMultisig implements MultisigWalletImport, KeystoreImport, WalletExport {
private final Gson gson = new Gson();
@Override
public String getName() {
return "Coldcard (Multisig)";
}
@Override
public PolicyType getPolicyType() {
return PolicyType.MULTI;
}
@Override
public Keystore getKeystore(ScriptType scriptType, InputStream inputStream) throws ImportException {
InputStreamReader reader = new InputStreamReader(inputStream);
ColdcardKeystore cck = Storage.getStorage().getGson().fromJson(reader, ColdcardKeystore.class);
Keystore keystore = new Keystore("Coldcard " + cck.xfp);
if(scriptType.equals(ScriptType.P2SH)) {
keystore.setKeyDerivation(new KeyDerivation(cck.xfp, cck.p2sh_deriv));
keystore.setExtendedPublicKey(ExtendedPublicKey.fromDescriptor(cck.p2sh));
} else if(scriptType.equals(ScriptType.P2SH_P2WSH)) {
keystore.setKeyDerivation(new KeyDerivation(cck.xfp, cck.p2wsh_p2sh_deriv));
keystore.setExtendedPublicKey(ExtendedPublicKey.fromDescriptor(cck.p2wsh_p2sh));
} else if(scriptType.equals(ScriptType.P2WSH)) {
keystore.setKeyDerivation(new KeyDerivation(cck.xfp, cck.p2wsh_deriv));
keystore.setExtendedPublicKey(ExtendedPublicKey.fromDescriptor(cck.p2wsh));
} else {
throw new ImportException("Correct derivation not found for script type: " + scriptType);
}
return keystore;
}
public static class ColdcardKeystore {
public String p2sh_deriv;
public String p2sh;
public String p2wsh_p2sh_deriv;
public String p2wsh_p2sh;
public String p2wsh_deriv;
public String p2wsh;
public String xfp;
}
@Override
public String getKeystoreImportDescription() {
return "Import file created by using the Settings > Multisig Wallets > Export XPUB feature on your Coldcard";
}
@Override
public Wallet importWallet(InputStream inputStream) throws ImportException {
Wallet wallet = new Wallet();
wallet.setPolicyType(PolicyType.MULTI);
int threshold = 2;
ScriptType scriptType = null;
String derivation = null;
try {
List<String> lines = CharStreams.readLines(new InputStreamReader(inputStream));
for (String line : lines) {
line = line.trim();
if (line.isEmpty()) {
continue;
}
String[] keyValue = line.split(":");
if (keyValue.length == 2) {
String key = keyValue[0].trim();
String value = keyValue[1].trim();
switch (key) {
case "Name":
wallet.setName(value.trim());
break;
case "Policy":
threshold = Integer.parseInt(value.split(" ")[0]);
break;
case "Derivation":
case "# derivation":
derivation = value;
break;
case "Format":
scriptType = ScriptType.valueOf(value.replace("P2WSH-P2SH", "P2SH_P2WSH"));
break;
default:
if (key.length() == 8 && Utils.isHex(key)) {
Keystore keystore = new Keystore("Coldcard " + key);
keystore.setKeyDerivation(new KeyDerivation(key, derivation));
keystore.setExtendedPublicKey(ExtendedPublicKey.fromDescriptor(value));
wallet.getKeystores().add(keystore);
}
}
}
}
Policy policy = Policy.getPolicy(PolicyType.MULTI, scriptType, wallet.getKeystores(), threshold);
wallet.setDefaultPolicy(policy);
wallet.setScriptType(scriptType);
return wallet;
} catch(Exception e) {
throw new ImportException(e);
}
}
@Override
public String getWalletImportDescription() {
return "Import file created by using the Settings > Multisig Wallets > [Wallet Detail] > Coldcard Export feature on your Coldcard";
}
@Override
public void exportWallet(Wallet wallet, OutputStream outputStream) throws ExportException {
if(!wallet.isValid()) {
throw new ExportException("Cannot export an incomplete wallet");
}
if(!wallet.getPolicyType().equals(PolicyType.MULTI)) {
throw new ExportException("Coldcard multisig import requires a multisig wallet");
}
boolean multipleDerivations = false;
Set<String> derivationSet = new HashSet<>();
for(Keystore keystore : wallet.getKeystores()) {
derivationSet.add(keystore.getKeyDerivation().getDerivationPath());
}
if(derivationSet.size() > 1) {
multipleDerivations = true;
}
try {
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(outputStream));
writer.append("# Coldcard Multisig setup file (created by Sparrow)\n");
writer.append("#\n");
writer.append("Name: ").append(wallet.getName()).append("\n");
writer.append("Policy: ").append(Integer.toString(wallet.getDefaultPolicy().getNumSignaturesRequired())).append(" of ").append(Integer.toString(wallet.getKeystores().size())).append("\n");
if(!multipleDerivations) {
writer.append("Derivation: ").append(wallet.getKeystores().get(0).getKeyDerivation().getDerivationPath()).append("\n");
}
writer.append("Format: ").append(wallet.getScriptType().toString().replace("P2SH-P2WSH", "P2WSH-P2SH")).append("\n");
writer.append("\n");
for(Keystore keystore : wallet.getKeystores()) {
if(multipleDerivations) {
writer.append("# derivation: ").append(keystore.getKeyDerivation().getDerivationPath()).append("\n");
}
writer.append(keystore.getKeyDerivation().getMasterFingerprint().toUpperCase()).append(": ").append(keystore.getExtendedPublicKey().toString()).append("\n");
if(multipleDerivations) {
writer.append("\n");
}
}
writer.flush();
writer.close();
} catch(Exception e) {
throw new ExportException(e);
}
}
@Override
public String getWalletExportDescription() {
return "Export file that can be read by your Coldcard using the Settings > Multisig Wallets > Import from SD feature";
}
}
@@ -0,0 +1,81 @@
package com.sparrowwallet.sparrow.external;
import com.google.common.io.CharStreams;
import com.sparrowwallet.drongo.ExtendedPublicKey;
import com.sparrowwallet.drongo.KeyDerivation;
import com.sparrowwallet.drongo.Utils;
import com.sparrowwallet.drongo.policy.Policy;
import com.sparrowwallet.drongo.policy.PolicyType;
import com.sparrowwallet.drongo.protocol.ScriptType;
import com.sparrowwallet.drongo.wallet.Keystore;
import com.sparrowwallet.drongo.wallet.Wallet;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.List;
import static com.sparrowwallet.drongo.protocol.ScriptType.*;
public class ColdcardSinglesig implements SinglesigWalletImport {
public static final List<ScriptType> ALLOWED_SCRIPT_TYPES = List.of(P2PKH, P2SH_P2WPKH, P2WPKH);
@Override
public String getName() {
return "Coldcard";
}
@Override
public Wallet importWallet(InputStream inputStream, ScriptType scriptType) throws ImportException {
if(!ALLOWED_SCRIPT_TYPES.contains(scriptType)) {
throw new ImportException("Script type of " + scriptType + " is not allowed");
}
Wallet wallet = new Wallet();
wallet.setPolicyType(PolicyType.SINGLE);
wallet.setScriptType(scriptType);
String masterFingerprint = null;
try {
List<String> lines = CharStreams.readLines(new InputStreamReader(inputStream));
for (String line : lines) {
line = line.trim();
if (line.isEmpty() || line.startsWith("#")) {
continue;
}
if(line.startsWith("xpub")) {
ExtendedPublicKey masterXpub = ExtendedPublicKey.fromDescriptor(line);
masterFingerprint = Utils.bytesToHex(masterXpub.getPubKey().getFingerprint()).toUpperCase();
wallet.setName("Coldcard " + masterFingerprint);
continue;
}
String[] keyValue = line.split("=>");
if(keyValue.length == 2) {
String key = keyValue[0].trim();
String value = keyValue[1].trim();
if(!key.equals("m") && scriptType.getDefaultDerivation().startsWith(key)) {
ExtendedPublicKey extPubKey = ExtendedPublicKey.fromDescriptor(value);
Keystore keystore = new Keystore();
keystore.setKeyDerivation(new KeyDerivation(masterFingerprint, key));
keystore.setExtendedPublicKey(extPubKey);
wallet.getKeystores().add(keystore);
break;
}
}
}
wallet.setDefaultPolicy(Policy.getPolicy(PolicyType.SINGLE, scriptType, wallet.getKeystores(), 1));
return wallet;
} catch(Exception e) {
throw new ImportException(e);
}
}
@Override
public String getWalletImportDescription() {
return "Import file created by using the Advanced > Dump Summary feature on your Coldcard";
}
}
@@ -0,0 +1,164 @@
package com.sparrowwallet.sparrow.external;
import com.google.gson.*;
import com.google.gson.reflect.TypeToken;
import com.sparrowwallet.drongo.ExtendedPublicKey;
import com.sparrowwallet.drongo.KeyDerivation;
import com.sparrowwallet.drongo.Utils;
import com.sparrowwallet.drongo.policy.Policy;
import com.sparrowwallet.drongo.policy.PolicyType;
import com.sparrowwallet.drongo.protocol.ScriptType;
import com.sparrowwallet.drongo.wallet.Keystore;
import com.sparrowwallet.drongo.wallet.Wallet;
import java.io.*;
import java.lang.reflect.Type;
import java.nio.charset.StandardCharsets;
import java.util.LinkedHashMap;
import java.util.Map;
public class Electrum implements SinglesigWalletImport, MultisigWalletImport, WalletExport {
@Override
public String getName() {
return "Electrum";
}
@Override
public Wallet importWallet(InputStream inputStream) throws ImportException {
InputStreamReader reader = new InputStreamReader(inputStream);
try {
Gson gson = new Gson();
Type stringStringMap = new TypeToken<Map<String, JsonElement>>(){}.getType();
Map<String,JsonElement> map = gson.fromJson(reader, stringStringMap);
ElectrumJsonWallet ew = new ElectrumJsonWallet();
ew.wallet_type = map.get("wallet_type").getAsString();
for(String key : map.keySet()) {
if(key.startsWith("x") || key.equals("keystore")) {
ElectrumKeystore ek = gson.fromJson(map.get(key), ElectrumKeystore.class);
if(ek.root_fingerprint == null && ek.ckcc_xfp != null) {
byte[] le = new byte[4];
Utils.uint32ToByteArrayLE(Long.parseLong(ek.ckcc_xfp), le, 0);
ek.root_fingerprint = Utils.bytesToHex(le).toUpperCase();
}
ew.keystores.put(key, ek);
}
}
Wallet wallet = new Wallet();
ScriptType scriptType = null;
for(ElectrumKeystore ek : ew.keystores.values()) {
Keystore keystore = new Keystore(ek.label);
keystore.setKeyDerivation(new KeyDerivation(ek.root_fingerprint, ek.derivation));
keystore.setExtendedPublicKey(ExtendedPublicKey.fromDescriptor(ek.xpub));
wallet.getKeystores().add(keystore);
ExtendedPublicKey.XpubHeader xpubHeader = ExtendedPublicKey.XpubHeader.fromXpub(ek.xpub);
scriptType = xpubHeader.getDefaultScriptType();
}
wallet.setScriptType(scriptType);
if(ew.wallet_type.equals("standard")) {
wallet.setPolicyType(PolicyType.SINGLE);
wallet.setDefaultPolicy(Policy.getPolicy(PolicyType.SINGLE, scriptType, wallet.getKeystores(), 1));
} else if(ew.wallet_type.contains("of")) {
wallet.setPolicyType(PolicyType.MULTI);
String[] mOfn = ew.wallet_type.split("of");
int threshold = Integer.parseInt(mOfn[0]);
wallet.setDefaultPolicy(Policy.getPolicy(PolicyType.MULTI, scriptType, wallet.getKeystores(), threshold));
} else {
throw new ImportException("Unknown Electrum wallet type of " + ew.wallet_type);
}
if(!wallet.isValid()) {
throw new IllegalStateException("Electrum wallet is in an inconsistent state");
}
return wallet;
} catch (Exception e) {
throw new ImportException(e);
}
}
@Override
public String getWalletImportDescription() {
return "Import an Electrum wallet";
}
@Override
public Wallet importWallet(InputStream inputStream, ScriptType scriptType) throws ImportException {
Wallet wallet = importWallet(inputStream);
wallet.setScriptType(scriptType);
return wallet;
}
@Override
public void exportWallet(Wallet wallet, OutputStream outputStream) throws ExportException {
try {
ElectrumJsonWallet ew = new ElectrumJsonWallet();
if(wallet.getPolicyType().equals(PolicyType.SINGLE)) {
ew.wallet_type = "standard";
} else if(wallet.getPolicyType().equals(PolicyType.MULTI)) {
ew.wallet_type = wallet.getDefaultPolicy().getNumSignaturesRequired() + "of" + wallet.getKeystores().size();
} else {
throw new ExportException("Could not export a wallet with a " + wallet.getPolicyType() + " policy");
}
ExtendedPublicKey.XpubHeader xpubHeader = ExtendedPublicKey.XpubHeader.fromScriptType(wallet.getScriptType());
int index = 1;
for(Keystore keystore : wallet.getKeystores()) {
ElectrumKeystore ek = new ElectrumKeystore();
ek.xpub = keystore.getExtendedPublicKey().toString(xpubHeader);
ek.derivation = keystore.getKeyDerivation().getDerivationPath();
ek.root_fingerprint = keystore.getKeyDerivation().getMasterFingerprint();
ek.label = keystore.getLabel();
if(wallet.getPolicyType().equals(PolicyType.SINGLE)) {
ew.keystores.put("keystore", ek);
} else if(wallet.getPolicyType().equals(PolicyType.MULTI)) {
ew.keystores.put("x" + index + "/", ek);
}
index++;
}
Gson gson = new Gson();
JsonObject eJson = gson.toJsonTree(ew.keystores).getAsJsonObject();
eJson.addProperty("wallet_type", ew.wallet_type);
gson = new GsonBuilder().setPrettyPrinting().disableHtmlEscaping().create();
String json = gson.toJson(eJson);
outputStream.write(json.getBytes(StandardCharsets.UTF_8));
outputStream.flush();
outputStream.close();
} catch (Exception e) {
throw new ExportException(e);
}
}
@Override
public String getWalletExportDescription() {
return "Export this wallet as an Electrum wallet file";
}
private static class ElectrumJsonWallet {
public Map<String, ElectrumKeystore> keystores = new LinkedHashMap<>();
public String wallet_type;
}
public static class ElectrumKeystore {
public String xpub;
public String hw_type;
public String ckcc_xfp;
public String root_fingerprint;
public String label;
public String soft_device_id;
public String type;
public String derivation;
}
}
@@ -0,0 +1,5 @@
package com.sparrowwallet.sparrow.external;
public interface Export {
String getName();
}
@@ -0,0 +1,19 @@
package com.sparrowwallet.sparrow.external;
public class ExportException extends Throwable {
public ExportException() {
super();
}
public ExportException(String message) {
super(message);
}
public ExportException(Throwable cause) {
super(cause);
}
public ExportException(String message, Throwable cause) {
super(message, cause);
}
}
@@ -0,0 +1,5 @@
package com.sparrowwallet.sparrow.external;
public interface Import {
String getName();
}
@@ -0,0 +1,19 @@
package com.sparrowwallet.sparrow.external;
public class ImportException extends Exception {
public ImportException() {
super();
}
public ImportException(String message) {
super(message);
}
public ImportException(Throwable cause) {
super(cause);
}
public ImportException(String message, Throwable cause) {
super(message, cause);
}
}
@@ -0,0 +1,13 @@
package com.sparrowwallet.sparrow.external;
import com.sparrowwallet.drongo.policy.PolicyType;
import com.sparrowwallet.drongo.protocol.ScriptType;
import com.sparrowwallet.drongo.wallet.Keystore;
import java.io.InputStream;
public interface KeystoreImport extends Import {
PolicyType getPolicyType();
Keystore getKeystore(ScriptType scriptType, InputStream inputStream) throws ImportException;
String getKeystoreImportDescription();
}
@@ -0,0 +1,10 @@
package com.sparrowwallet.sparrow.external;
import com.sparrowwallet.drongo.wallet.Wallet;
import java.io.InputStream;
public interface MultisigWalletImport extends Import {
String getWalletImportDescription();
Wallet importWallet(InputStream inputStream) throws ImportException;
}
@@ -0,0 +1,11 @@
package com.sparrowwallet.sparrow.external;
import com.sparrowwallet.drongo.protocol.ScriptType;
import com.sparrowwallet.drongo.wallet.Wallet;
import java.io.InputStream;
public interface SinglesigWalletImport extends Import {
String getWalletImportDescription();
Wallet importWallet(InputStream inputStream, ScriptType scriptType) throws ImportException;
}
@@ -0,0 +1,10 @@
package com.sparrowwallet.sparrow.external;
import com.sparrowwallet.drongo.wallet.Wallet;
import java.io.OutputStream;
public interface WalletExport extends Export {
void exportWallet(Wallet wallet, OutputStream outputStream) throws ExportException;
String getWalletExportDescription();
}
@@ -36,6 +36,10 @@ public class Storage {
return SINGLETON;
}
public Gson getGson() {
return gson;
}
public Wallet loadWallet(File file) throws IOException {
Reader reader = new FileReader(file);
Wallet wallet = gson.fromJson(reader, Wallet.class);
@@ -44,18 +48,6 @@ public class Storage {
return wallet;
}
public static final void main(String[] args) throws Exception {
File file = new File("/Users/scy/.electrum-latest/wallets/scyone");
ECKey pubKey = ECKey.createKeyPbkdf2HmacSha512("***REMOVED***");
BufferedInputStream inputStream = new BufferedInputStream(new FileInputStream(file));
byte[] encrypted = ByteStreams.toByteArray(inputStream);
byte[] decrypted = pubKey.decryptEcies(encrypted, getEncryptionMagic());
String jsonWallet = inflate(decrypted);
System.out.println(jsonWallet);
}
public Wallet loadWallet(File file, ECKey encryptionKey) throws IOException {
BufferedInputStream inputStream = new BufferedInputStream(new FileInputStream(file));
byte[] encrypted = ByteStreams.toByteArray(inputStream);