Add nthKey support

Keys can be imported using JSON or QR.

Wallet can be exported using JSON or QR.

Device supports testnet, signet and mainnet.

Only sh-wpkh (native Segwit) is supported.

Only multisig is supported.

Thumbnails generated with:
  convert nthkey-orig.png -channel RGB -negate -resize 300x300 -background white -gravity center -extent 200x200 -type grayscale nthkey@3x.png
  convert nthkey-orig.png -channel RGB -negate -resize 150x150 -background white -gravity center -extent 100x100 -type grayscale nthkey@2x.png
  convert nthkey-orig.png -channel RGB -negate -resize 75x75 -background white -gravity center -extent 50x50 -type grayscale nthkey.png
This commit is contained in:
Sjors Provoost 2021-08-24 17:47:24 +02:00
parent b22e891b7d
commit 15d1183cce
No known key found for this signature in database
GPG key ID: 57FF9BDBCC301009
9 changed files with 247 additions and 4 deletions

View file

@ -938,7 +938,8 @@ public class AppController implements Initializable {
new CoboVaultSinglesig(), new CoboVaultMultisig(),
new PassportSinglesig(),
new KeystoneSinglesig(), new KeystoneMultisig(),
new CaravanMultisig());
new CaravanMultisig(),
new NthKeyMultisig());
for(WalletImport importer : walletImporters) {
try(FileInputStream inputStream = new FileInputStream(file)) {
if(importer.isEncrypted(file) && password == null) {

View file

@ -44,7 +44,7 @@ public class WalletExportDialog extends Dialog<Wallet> {
if(wallet.getPolicyType() == PolicyType.SINGLE) {
exporters = List.of(new Electrum(), new SpecterDesktop(), new Sparrow());
} else if(wallet.getPolicyType() == PolicyType.MULTI) {
exporters = List.of(new CaravanMultisig(), new ColdcardMultisig(), new CoboVaultMultisig(), new Electrum(), new KeystoneMultisig(), new PassportMultisig(), new SpecterDesktop(), new BlueWalletMultisig(), new SpecterDIY(), new Sparrow());
exporters = List.of(new CaravanMultisig(), new ColdcardMultisig(), new CoboVaultMultisig(), new Electrum(), new KeystoneMultisig(), new PassportMultisig(), new SpecterDesktop(), new BlueWalletMultisig(), new SpecterDIY(), new Sparrow(), new NthKeyMultisig());
} else {
throw new UnsupportedOperationException("Cannot export wallet with policy type " + wallet.getPolicyType());
}

View file

@ -54,7 +54,7 @@ public class WalletImportDialog extends Dialog<Wallet> {
importAccordion.getPanes().add(importPane);
}
List<WalletImport> walletImporters = List.of(new CaravanMultisig(), new ColdcardMultisig(), new CoboVaultMultisig(), new Electrum(), new KeystoneMultisig(), new SpecterDesktop(), new BlueWalletMultisig(), new Sparrow());
List<WalletImport> walletImporters = List.of(new CaravanMultisig(), new ColdcardMultisig(), new CoboVaultMultisig(), new Electrum(), new KeystoneMultisig(), new SpecterDesktop(), new BlueWalletMultisig(), new Sparrow(), new NthKeyMultisig());
for(WalletImport importer : walletImporters) {
FileWalletImportPane importPane = new FileWalletImportPane(importer);
importAccordion.getPanes().add(importPane);

View file

@ -0,0 +1,242 @@
package com.sparrowwallet.sparrow.io;
import com.google.common.io.CharStreams;
import com.sparrowwallet.drongo.ExtendedKey;
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.KeystoreSource;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.drongo.wallet.WalletModel;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
public class NthKeyMultisig implements WalletImport, KeystoreFileImport, WalletExport {
private static final Logger log = LoggerFactory.getLogger(NthKeyMultisig.class);
@Override
public String getName() {
return "nthKey Multisig";
}
@Override
public WalletModel getWalletModel() {
return WalletModel.NTHKEY;
}
@Override
public Keystore getKeystore(ScriptType scriptType, InputStream inputStream, String password) throws ImportException {
InputStreamReader reader = new InputStreamReader(inputStream, StandardCharsets.UTF_8);
NthKeyKeystore cck = JsonPersistence.getGson().fromJson(reader, NthKeyKeystore.class);
Keystore keystore = new Keystore("NthKey");
keystore.setSource(KeystoreSource.HW_AIRGAPPED);
keystore.setWalletModel(WalletModel.NTHKEY);
if(cck.xpub != null && cck.path != null) {
ExtendedKey.Header header = ExtendedKey.Header.fromExtendedKey(cck.xpub);
if(header.getDefaultScriptType() != scriptType) {
throw new ImportException("This wallet's script type (" + scriptType + ") does not match the " + getName() + " script type (" + header.getDefaultScriptType() + ")");
}
keystore.setKeyDerivation(new KeyDerivation(cck.xfp, cck.path));
keystore.setExtendedPublicKey(ExtendedKey.fromDescriptor(cck.xpub));
} else if(scriptType.equals(ScriptType.P2SH)) {
keystore.setKeyDerivation(new KeyDerivation(cck.xfp, cck.p2sh_deriv));
keystore.setExtendedPublicKey(ExtendedKey.fromDescriptor(cck.p2sh));
} else if(scriptType.equals(ScriptType.P2SH_P2WSH)) {
keystore.setKeyDerivation(new KeyDerivation(cck.xfp, cck.p2wsh_p2sh_deriv != null ? cck.p2wsh_p2sh_deriv : cck.p2sh_p2wsh_deriv));
keystore.setExtendedPublicKey(ExtendedKey.fromDescriptor(cck.p2wsh_p2sh != null ? cck.p2wsh_p2sh : cck.p2sh_p2wsh));
} else if(scriptType.equals(ScriptType.P2WSH)) {
keystore.setKeyDerivation(new KeyDerivation(cck.xfp, cck.p2wsh_deriv));
keystore.setExtendedPublicKey(ExtendedKey.fromDescriptor(cck.p2wsh));
} else {
throw new ImportException("Correct derivation not found for script type: " + scriptType);
}
return keystore;
}
private static class NthKeyKeystore {
public String p2sh_deriv;
public String p2sh;
public String p2wsh_p2sh_deriv;
public String p2wsh_p2sh;
public String p2sh_p2wsh_deriv;
public String p2sh_p2wsh;
public String p2wsh_deriv;
public String p2wsh;
public String xpub;
public String path;
public String xfp;
}
@Override
public String getKeystoreImportDescription() {
return "Import file created by using the Settings > Announce feature in nthKey.";
}
@Override
public String getExportFileExtension(Wallet wallet) {
return "txt";
}
@Override
public Wallet importWallet(InputStream inputStream, String password) throws ImportException {
Wallet wallet = new Wallet();
wallet.setPolicyType(PolicyType.MULTI);
int threshold = 2;
ScriptType scriptType = ScriptType.P2SH;
String derivation = null;
try {
List<String> lines = CharStreams.readLines(new InputStreamReader(inputStream, StandardCharsets.UTF_8));
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("P2SH_P2WSH"));
break;
default:
if (key.length() == 8 && Utils.isHex(key)) {
Keystore keystore = new Keystore("NthKey");
keystore.setSource(KeystoreSource.HW_AIRGAPPED);
keystore.setWalletModel(WalletModel.NTHKEY);
keystore.setKeyDerivation(new KeyDerivation(key, derivation));
keystore.setExtendedPublicKey(ExtendedKey.fromDescriptor(value));
wallet.makeLabelsUnique(keystore);
wallet.getKeystores().add(keystore);
}
}
}
}
Policy policy = Policy.getPolicy(PolicyType.MULTI, scriptType, wallet.getKeystores(), threshold);
wallet.setDefaultPolicy(policy);
wallet.setScriptType(scriptType);
if(!wallet.isValid()) {
throw new IllegalStateException("This file does not describe a valid wallet. " + getKeystoreImportDescription());
}
return wallet;
} catch(Exception e) {
log.error("Error importing " + getName() + " wallet", e);
throw new ImportException("Error importing " + getName() + " wallet", e);
}
}
@Override
public String getWalletImportDescription() {
return "Import file created by using the Settings > Announce -> Save as JSON Mainnet in nthKey.";
}
@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(getName() + " 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 {
// TODO: should produce a JSON file like Specter does
// BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(outputStream, StandardCharsets.UTF_8));
// writer.append("# " + getName() + " setup file (created by Sparrow)\n");
// writer.append("#\n");
// writer.append("Name: ").append(wallet.getFullName()).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();
} catch(Exception e) {
log.error("Error exporting " + getName() + " wallet", e);
throw new ExportException("Error exporting " + getName() + " wallet", e);
}
}
@Override
public String getWalletExportDescription() {
return "Export file that can be read by nthKey using the Settings > Import wallet > Import wallet JSON.";
}
@Override
public boolean isEncrypted(File file) {
return false;
}
@Override
public boolean isWalletImportScannable() {
return false;
}
@Override
public boolean isKeystoreImportScannable() {
return true;
}
@Override
public boolean isWalletExportScannable() {
return true;
}
@Override
public boolean walletExportRequiresDecryption() {
return false;
}
}

View file

@ -73,7 +73,7 @@ public class SpecterDesktop implements WalletImport, WalletExport {
keystore.setWalletModel(walletModel);
if(walletModel == WalletModel.TREZOR_1 || walletModel == WalletModel.TREZOR_T || walletModel == WalletModel.KEEPKEY ||
walletModel == WalletModel.LEDGER_NANO_S || walletModel == WalletModel.LEDGER_NANO_X ||
walletModel == WalletModel.BITBOX_02 || walletModel == WalletModel.COLDCARD) {
walletModel == WalletModel.BITBOX_02 || walletModel == WalletModel.COLDCARD || walletModel == WalletModel.NTHKEY) {
keystore.setSource(KeystoreSource.HW_USB);
} else if(walletModel == WalletModel.BITCOIN_CORE) {
keystore.setSource(KeystoreSource.SW_WATCH);

Binary file not shown.

After

Width:  |  Height:  |  Size: 644 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB