encrypt electrum wallet exports including private keys where password is available

This commit is contained in:
Craig Raw 2024-01-08 09:16:55 +02:00
parent 4feb4a3a79
commit b5196d1ac2
19 changed files with 83 additions and 39 deletions

View file

@ -122,16 +122,12 @@ public class FileWalletExportPane extends TitledDescriptionPane {
Optional<SecureString> password = dlg.showAndWait();
if(password.isPresent()) {
final String walletId = AppServices.get().getOpenWallets().get(wallet).getWalletId(wallet);
String walletPassword = password.get().asString();
Storage.DecryptWalletService decryptWalletService = new Storage.DecryptWalletService(copy, password.get());
decryptWalletService.setOnSucceeded(workerStateEvent -> {
EventManager.get().post(new StorageEvent(walletId, TimedEvent.Action.END, "Done"));
Wallet decryptedWallet = decryptWalletService.getValue();
try {
exportWallet(file, decryptedWallet);
} finally {
decryptedWallet.clearPrivate();
}
exportWallet(file, decryptedWallet, walletPassword);
});
decryptWalletService.setOnFailed(workerStateEvent -> {
EventManager.get().post(new StorageEvent(walletId, TimedEvent.Action.END, "Failed"));
@ -141,14 +137,14 @@ public class FileWalletExportPane extends TitledDescriptionPane {
decryptWalletService.start();
}
} else {
exportWallet(file, wallet);
exportWallet(file, wallet, null);
}
}
private void exportWallet(File file, Wallet exportWallet) {
private void exportWallet(File file, Wallet exportWallet, String password) {
try {
if(file != null) {
FileWalletExportService fileWalletExportService = new FileWalletExportService(exporter, file, exportWallet);
FileWalletExportService fileWalletExportService = new FileWalletExportService(exporter, file, exportWallet, password);
fileWalletExportService.setOnSucceeded(event -> {
EventManager.get().post(new WalletExportEvent(exportWallet));
});
@ -163,7 +159,7 @@ public class FileWalletExportPane extends TitledDescriptionPane {
fileWalletExportService.start();
} else {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
exporter.exportWallet(exportWallet, outputStream);
exporter.exportWallet(exportWallet, outputStream, password);
QRDisplayDialog qrDisplayDialog;
if(exporter instanceof CoboVaultMultisig) {
qrDisplayDialog = new QRDisplayDialog(RegistryType.BYTES.toString(), outputStream.toByteArray(), true);
@ -185,6 +181,10 @@ public class FileWalletExportPane extends TitledDescriptionPane {
errorMessage = e.getCause().getMessage();
}
setError("Export Error", errorMessage);
} finally {
if(file == null && password != null) {
exportWallet.clearPrivate();
}
}
}
@ -192,11 +192,13 @@ public class FileWalletExportPane extends TitledDescriptionPane {
private final WalletExport exporter;
private final File file;
private final Wallet wallet;
private final String password;
public FileWalletExportService(WalletExport exporter, File file, Wallet wallet) {
public FileWalletExportService(WalletExport exporter, File file, Wallet wallet, String password) {
this.exporter = exporter;
this.file = file;
this.wallet = wallet;
this.password = password;
}
@Override
@ -205,7 +207,11 @@ public class FileWalletExportPane extends TitledDescriptionPane {
@Override
protected Void call() throws Exception {
try(OutputStream outputStream = new FileOutputStream(file)) {
exporter.exportWallet(wallet, outputStream);
exporter.exportWallet(wallet, outputStream, password);
} finally {
if(password != null) {
wallet.clearPrivate();
}
}
return null;

View file

@ -189,7 +189,7 @@ public class Bip129 implements KeystoreFileExport, KeystoreFileImport, WalletExp
}
@Override
public void exportWallet(Wallet wallet, OutputStream outputStream) throws ExportException {
public void exportWallet(Wallet wallet, OutputStream outputStream, String password) throws ExportException {
try {
String record = "BSMS 1.0\n" +
OutputDescriptor.getOutputDescriptor(wallet) +

View file

@ -94,7 +94,7 @@ public class CaravanMultisig implements WalletImport, WalletExport {
}
@Override
public void exportWallet(Wallet wallet, OutputStream outputStream) throws ExportException {
public void exportWallet(Wallet wallet, OutputStream outputStream, String password) throws ExportException {
if(!wallet.isValid()) {
throw new ExportException("Cannot export an incomplete wallet");
}

View file

@ -165,7 +165,7 @@ public class ColdcardMultisig implements WalletImport, KeystoreFileImport, Walle
}
@Override
public void exportWallet(Wallet wallet, OutputStream outputStream) throws ExportException {
public void exportWallet(Wallet wallet, OutputStream outputStream, String password) throws ExportException {
if(!wallet.isValid()) {
throw new ExportException("Cannot export an incomplete wallet");
}

View file

@ -23,7 +23,7 @@ public class Descriptor implements WalletImport, WalletExport {
}
@Override
public void exportWallet(Wallet wallet, OutputStream outputStream) throws ExportException {
public void exportWallet(Wallet wallet, OutputStream outputStream, String password) throws ExportException {
try {
BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(outputStream));
bufferedWriter.write("# Receive and change descriptor (BIP389):");

View file

@ -2,10 +2,7 @@ package com.sparrowwallet.sparrow.io;
import com.google.gson.*;
import com.google.gson.reflect.TypeToken;
import com.sparrowwallet.drongo.ExtendedKey;
import com.sparrowwallet.drongo.KeyDerivation;
import com.sparrowwallet.drongo.KeyPurpose;
import com.sparrowwallet.drongo.Utils;
import com.sparrowwallet.drongo.*;
import com.sparrowwallet.drongo.address.Address;
import com.sparrowwallet.drongo.crypto.*;
import com.sparrowwallet.drongo.policy.Policy;
@ -18,7 +15,10 @@ import org.slf4j.LoggerFactory;
import java.io.*;
import java.lang.reflect.Type;
import java.nio.charset.StandardCharsets;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.*;
import java.util.zip.DeflaterOutputStream;
import java.util.zip.InflaterInputStream;
public class Electrum implements KeystoreFileImport, WalletImport, WalletExport {
@ -301,11 +301,11 @@ public class Electrum implements KeystoreFileImport, WalletImport, WalletExport
@Override
public String getExportFileExtension(Wallet wallet) {
return "json";
return wallet.isEncrypted() ? "" : "json";
}
@Override
public void exportWallet(Wallet wallet, OutputStream outputStream) throws ExportException {
public void exportWallet(Wallet wallet, OutputStream outputStream, String password) throws ExportException {
try {
ElectrumJsonWallet ew = new ElectrumJsonWallet();
if(wallet.getPolicyType().equals(PolicyType.SINGLE)) {
@ -342,7 +342,18 @@ public class Electrum implements KeystoreFileImport, WalletImport, WalletExport
ek.seed = keystore.getSeed().getMnemonicString().asString();
ek.passphrase = keystore.getSeed().getPassphrase() == null ? null : keystore.getSeed().getPassphrase().asString();
}
if(password != null) {
ek.xprv = encrypt(ek.xprv, password);
if(ek.seed != null) {
ek.seed = encrypt(ek.seed, password);
}
if(ek.passphrase != null) {
ek.passphrase = encrypt(ek.passphrase, password);
}
ew.use_encryption = true;
} else {
ew.use_encryption = false;
}
} else if(keystore.getSource() == KeystoreSource.SW_WATCH) {
ek.type = "bip32";
ek.xpub = keystore.getExtendedPublicKey().toString(xpubHeader);
@ -373,14 +384,41 @@ public class Electrum implements KeystoreFileImport, WalletImport, WalletExport
gson = new GsonBuilder().setPrettyPrinting().disableHtmlEscaping().create();
String json = gson.toJson(eJson);
if(password != null) {
ECKey encryptionKey = Pbkdf2KeyDeriver.DEFAULT_INSTANCE.deriveECKey(password);
outputStream = new DeflaterOutputStream(new ECIESOutputStream(outputStream, encryptionKey));
}
outputStream.write(json.getBytes(StandardCharsets.UTF_8));
outputStream.flush();
outputStream.close();
} catch (Exception e) {
log.error("Error exporting Electrum Wallet", e);
throw new ExportException("Error exporting Electrum Wallet", e);
}
}
private String encrypt(String plain, String password) throws NoSuchAlgorithmException {
if(plain == null) {
return null;
}
byte[] plainBytes = plain.getBytes(StandardCharsets.UTF_8);
KeyDeriver keyDeriver = new DoubleSha256KeyDeriver();
Key key = keyDeriver.deriveKey(password);
KeyCrypter keyCrypter = new AESKeyCrypter();
byte[] initializationVector = new byte[16];
SecureRandom.getInstanceStrong().nextBytes(initializationVector);
EncryptedData encryptedData = keyCrypter.encrypt(plainBytes, initializationVector, key);
byte[] encrypted = new byte[initializationVector.length + encryptedData.getEncryptedBytes().length];
System.arraycopy(initializationVector, 0, encrypted, 0, 16);
System.arraycopy(encryptedData.getEncryptedBytes(), 0, encrypted, 16, encryptedData.getEncryptedBytes().length);
byte[] encryptedBase64 = Base64.getEncoder().encode(encrypted);
return new String(encryptedBase64, StandardCharsets.UTF_8);
}
@Override
public boolean isEncrypted(File file) {
return (FileType.BINARY.equals(IOUtils.getFileType(file)));

View file

@ -29,7 +29,7 @@ public class ElectrumPersonalServer implements WalletExport {
}
@Override
public void exportWallet(Wallet wallet, OutputStream outputStream) throws ExportException {
public void exportWallet(Wallet wallet, OutputStream outputStream, String password) throws ExportException {
if(wallet.getScriptType() == ScriptType.P2TR) {
throw new ExportException(getName() + " does not support Taproot wallets.");
}

View file

@ -28,7 +28,7 @@ public class Sparrow implements WalletImport, WalletExport {
}
@Override
public void exportWallet(Wallet wallet, OutputStream outputStream) throws ExportException {
public void exportWallet(Wallet wallet, OutputStream outputStream, String password) throws ExportException {
try {
Wallet exportedWallet = !wallet.isMasterWallet() ? wallet.getMasterWallet() : wallet;
PersistenceType persistenceType = PersistenceType.DB;

View file

@ -65,7 +65,7 @@ public class SpecterDIY implements KeystoreFileImport, WalletExport {
}
@Override
public void exportWallet(Wallet wallet, OutputStream outputStream) throws ExportException {
public void exportWallet(Wallet wallet, OutputStream outputStream, String password) throws ExportException {
try {
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(outputStream, StandardCharsets.UTF_8));
writer.append("addwallet ").append(wallet.getFullName()).append("&").append(OutputDescriptor.getOutputDescriptor(wallet).toString().replace('\'', 'h')).append("\n");

View file

@ -18,7 +18,7 @@ public class SpecterDesktop implements WalletImport, WalletExport {
private static final Logger log = LoggerFactory.getLogger(SpecterDesktop.class);
@Override
public void exportWallet(Wallet wallet, OutputStream outputStream) throws ExportException {
public void exportWallet(Wallet wallet, OutputStream outputStream, String password) throws ExportException {
try {
SpecterWallet specterWallet = new SpecterWallet();
specterWallet.label = wallet.getFullName();

View file

@ -752,7 +752,7 @@ public class Storage {
protected Void call() throws IOException, ExportException {
Sparrow export = new Sparrow();
try(BufferedOutputStream outputStream = new BufferedOutputStream(new FileOutputStream(newWalletFile))) {
export.exportWallet(wallet, outputStream);
export.exportWallet(wallet, outputStream, null);
}
return null;

View file

@ -5,7 +5,7 @@ import com.sparrowwallet.drongo.wallet.Wallet;
import java.io.OutputStream;
public interface WalletExport extends ImportExport {
void exportWallet(Wallet wallet, OutputStream outputStream) throws ExportException;
void exportWallet(Wallet wallet, OutputStream outputStream, String password) throws ExportException;
String getWalletExportDescription();
String getExportFileExtension(Wallet wallet);
boolean isWalletExportScannable();

View file

@ -46,7 +46,7 @@ public class WalletLabels implements WalletImport, WalletExport {
}
@Override
public void exportWallet(Wallet wallet, OutputStream outputStream) throws ExportException {
public void exportWallet(Wallet wallet, OutputStream outputStream, String password) throws ExportException {
List<Label> labels = new ArrayList<>();
List<Wallet> allWallets = wallet.isMasterWallet() ? wallet.getAllWallets() : wallet.getMasterWallet().getAllWallets();
for(Wallet exportWallet : allWallets) {

View file

@ -44,7 +44,7 @@ public class WalletTransactions implements WalletExport {
}
@Override
public void exportWallet(Wallet wallet, OutputStream outputStream) throws ExportException {
public void exportWallet(Wallet wallet, OutputStream outputStream, String password) throws ExportException {
WalletTransactionsEntry walletTransactionsEntry = walletForm.getWalletTransactionsEntry();
ExchangeSource exchangeSource = Config.get().getExchangeSource();

View file

@ -114,7 +114,7 @@ public class TransactionsController extends WalletFormController implements Init
AppServices.moveToActiveWindowScreen(window, 800, 450);
File file = fileChooser.showSaveDialog(window);
if(file != null) {
FileWalletExportPane.FileWalletExportService exportService = new FileWalletExportPane.FileWalletExportService(new WalletTransactions(getWalletForm()), file, wallet);
FileWalletExportPane.FileWalletExportService exportService = new FileWalletExportPane.FileWalletExportService(new WalletTransactions(getWalletForm()), file, wallet, null);
exportService.setOnFailed(failedEvent -> {
Throwable e = failedEvent.getSource().getException();
log.error("Error exporting transactions as CSV", e);

View file

@ -36,7 +36,7 @@ public class CaravanMultisigTest extends IoTest {
byte[] walletBytes = ByteStreams.toByteArray(getInputStream("caravan-multisig-export-1.json"));
Wallet wallet = ccMultisig.importWallet(new ByteArrayInputStream(walletBytes), null);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ccMultisig.exportWallet(wallet, baos);
ccMultisig.exportWallet(wallet, baos, null);
byte[] exportedBytes = baos.toByteArray();
String original = new String(walletBytes);
String exported = new String(exportedBytes);

View file

@ -105,7 +105,7 @@ public class ColdcardMultisigTest extends IoTest {
byte[] walletBytes = ByteStreams.toByteArray(getInputStream("cc-multisig-export-1.txt"));
Wallet wallet = ccMultisig.importWallet(new ByteArrayInputStream(walletBytes), null);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ccMultisig.exportWallet(wallet, baos);
ccMultisig.exportWallet(wallet, baos, null);
byte[] exportedBytes = baos.toByteArray();
String original = new String(walletBytes);
String exported = new String(exportedBytes);
@ -118,7 +118,7 @@ public class ColdcardMultisigTest extends IoTest {
byte[] walletBytes = ByteStreams.toByteArray(getInputStream("cc-multisig-export-multideriv.txt"));
Wallet wallet = ccMultisig.importWallet(new ByteArrayInputStream(walletBytes), null);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ccMultisig.exportWallet(wallet, baos);
ccMultisig.exportWallet(wallet, baos, null);
byte[] exportedBytes = baos.toByteArray();
String original = new String(walletBytes);
String exported = new String(exportedBytes);

View file

@ -38,7 +38,7 @@ public class ElectrumTest extends IoTest {
byte[] walletBytes = ByteStreams.toByteArray(getInputStream("electrum-singlesig-wallet.json"));
Wallet wallet = electrum.importWallet(new ByteArrayInputStream(walletBytes), null);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
electrum.exportWallet(wallet, baos);
electrum.exportWallet(wallet, baos, null);
wallet = electrum.importWallet(new ByteArrayInputStream(baos.toByteArray()), null);
Assert.assertTrue(wallet.isValid());
@ -76,7 +76,7 @@ public class ElectrumTest extends IoTest {
byte[] walletBytes = ByteStreams.toByteArray(getInputStream("electrum-multisig-wallet.json"));
Wallet wallet = electrum.importWallet(new ByteArrayInputStream(walletBytes), null);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
electrum.exportWallet(wallet, baos);
electrum.exportWallet(wallet, baos, null);
wallet = electrum.importWallet(new ByteArrayInputStream(baos.toByteArray()), null);
Assert.assertTrue(wallet.isValid());
@ -113,7 +113,7 @@ public class ElectrumTest extends IoTest {
byte[] walletBytes = ByteStreams.toByteArray(getInputStream("electrum-singlesig-seed-wallet.json"));
Wallet wallet = electrum.importWallet(new ByteArrayInputStream(walletBytes), null);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
electrum.exportWallet(wallet, baos);
electrum.exportWallet(wallet, baos, null);
Assert.assertEquals("e14c40c638e2c83d1f20e5ee9cd744bc2ba1ef64fa939926f3778fc8735e891f56852f687b32bbd044f272d2831137e3eeba61fd1f285fa73dcc97d9f2be3cd1", Utils.bytesToHex(wallet.getKeystores().get(0).getSeed().getSeedBytes()));
wallet = electrum.importWallet(new ByteArrayInputStream(baos.toByteArray()), null);

View file

@ -37,7 +37,7 @@ public class SpecterDIYTest extends IoTest {
SpecterDIY specterDIY = new SpecterDIY();
byte[] walletBytes = ByteStreams.toByteArray(getInputStream("specter-diy-export.txt"));
ByteArrayOutputStream baos = new ByteArrayOutputStream();
specterDIY.exportWallet(wallet, baos);
specterDIY.exportWallet(wallet, baos, null);
String original = new String(walletBytes);
String exported = new String(baos.toByteArray());