mirror of
https://github.com/sparrowwallet/sparrow.git
synced 2024-11-04 21:36:45 +00:00
add support for lnurl-auth authentication by registering a platform uri handler
This commit is contained in:
parent
6efe5e4ccc
commit
80fab6df99
10 changed files with 327 additions and 19 deletions
|
@ -218,7 +218,7 @@ jlink {
|
||||||
appVersion = "${sparrowVersion}"
|
appVersion = "${sparrowVersion}"
|
||||||
skipInstaller = os.macOsX || properties.skipInstallers
|
skipInstaller = os.macOsX || properties.skipInstallers
|
||||||
imageOptions = []
|
imageOptions = []
|
||||||
installerOptions = ['--file-associations', 'src/main/deploy/psbt.properties', '--file-associations', 'src/main/deploy/txn.properties', '--file-associations', 'src/main/deploy/bitcoin.properties', '--file-associations', 'src/main/deploy/auth47.properties', '--license-file', 'LICENSE']
|
installerOptions = ['--file-associations', 'src/main/deploy/psbt.properties', '--file-associations', 'src/main/deploy/txn.properties', '--file-associations', 'src/main/deploy/bitcoin.properties', '--file-associations', 'src/main/deploy/auth47.properties', '--file-associations', 'src/main/deploy/lightning.properties', '--license-file', 'LICENSE']
|
||||||
if(os.windows) {
|
if(os.windows) {
|
||||||
installerOptions += ['--win-per-user-install', '--win-dir-chooser', '--win-menu', '--win-menu-group', 'Sparrow', '--win-shortcut', '--resource-dir', 'src/main/deploy/package/windows/']
|
installerOptions += ['--win-per-user-install', '--win-dir-chooser', '--win-menu', '--win-menu-group', 'Sparrow', '--win-shortcut', '--resource-dir', 'src/main/deploy/package/windows/']
|
||||||
imageOptions += ['--icon', 'src/main/deploy/package/windows/sparrow.ico']
|
imageOptions += ['--icon', 'src/main/deploy/package/windows/sparrow.ico']
|
||||||
|
|
2
drongo
2
drongo
|
@ -1 +1 @@
|
||||||
Subproject commit ddaf698c1011e5b01c7c3ed1e7693145ba5531ac
|
Subproject commit 8cdea77562643edf9d460a594178c1f44deeb248
|
2
src/main/deploy/lightning.properties
Normal file
2
src/main/deploy/lightning.properties
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
mime-type=x-scheme-handler/lightning
|
||||||
|
description=LNURL URI
|
|
@ -6,4 +6,4 @@ Icon=/opt/sparrow/lib/Sparrow.png
|
||||||
Terminal=false
|
Terminal=false
|
||||||
Type=Application
|
Type=Application
|
||||||
Categories=Unknown
|
Categories=Unknown
|
||||||
MimeType=application/psbt;application/bitcoin-transaction;x-scheme-handler/bitcoin;x-scheme-handler/auth47
|
MimeType=application/psbt;application/bitcoin-transaction;x-scheme-handler/bitcoin;x-scheme-handler/auth47;x-scheme-handler/lightning
|
|
@ -53,6 +53,14 @@
|
||||||
<string>auth47</string>
|
<string>auth47</string>
|
||||||
</array>
|
</array>
|
||||||
</dict>
|
</dict>
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleURLName</key>
|
||||||
|
<string>com.sparrowwallet.sparrow.lightning</string>
|
||||||
|
<key>CFBundleURLSchemes</key>
|
||||||
|
<array>
|
||||||
|
<string>lightning</string>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
</array>
|
</array>
|
||||||
<key>UTImportedTypeDeclarations</key>
|
<key>UTImportedTypeDeclarations</key>
|
||||||
<array>
|
<array>
|
||||||
|
|
|
@ -97,6 +97,16 @@
|
||||||
<RegistryValue Type="string" Value=""[INSTALLDIR]$(var.JpAppName).exe" "%1"" />
|
<RegistryValue Type="string" Value=""[INSTALLDIR]$(var.JpAppName).exe" "%1"" />
|
||||||
</RegistryKey>
|
</RegistryKey>
|
||||||
</RegistryKey>
|
</RegistryKey>
|
||||||
|
<RegistryKey Root="HKCR" Key="lightning" Action="createAndRemoveOnUninstall">
|
||||||
|
<RegistryValue Type="string" Name="URL Protocol" Value=""/>
|
||||||
|
<RegistryValue Type="string" Value="URL:LNURL URI"/>
|
||||||
|
<RegistryKey Key="DefaultIcon">
|
||||||
|
<RegistryValue Type="string" Value="$(var.JpAppName).exe" />
|
||||||
|
</RegistryKey>
|
||||||
|
<RegistryKey Key="shell\open\command">
|
||||||
|
<RegistryValue Type="string" Value=""[INSTALLDIR]$(var.JpAppName).exe" "%1"" />
|
||||||
|
</RegistryKey>
|
||||||
|
</RegistryKey>
|
||||||
</Component>
|
</Component>
|
||||||
</DirectoryRef>
|
</DirectoryRef>
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,16 @@ package com.sparrowwallet.sparrow;
|
||||||
import com.google.common.eventbus.Subscribe;
|
import com.google.common.eventbus.Subscribe;
|
||||||
import com.google.common.net.HostAndPort;
|
import com.google.common.net.HostAndPort;
|
||||||
import com.sparrowwallet.drongo.Network;
|
import com.sparrowwallet.drongo.Network;
|
||||||
|
import com.sparrowwallet.drongo.SecureString;
|
||||||
import com.sparrowwallet.drongo.address.Address;
|
import com.sparrowwallet.drongo.address.Address;
|
||||||
|
import com.sparrowwallet.drongo.bip47.PaymentCode;
|
||||||
|
import com.sparrowwallet.drongo.crypto.ECKey;
|
||||||
|
import com.sparrowwallet.drongo.crypto.EncryptionType;
|
||||||
|
import com.sparrowwallet.drongo.crypto.InvalidPasswordException;
|
||||||
|
import com.sparrowwallet.drongo.crypto.Key;
|
||||||
|
import com.sparrowwallet.drongo.policy.PolicyType;
|
||||||
|
import com.sparrowwallet.drongo.wallet.*;
|
||||||
|
import com.sparrowwallet.sparrow.control.WalletPasswordDialog;
|
||||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||||
import com.sparrowwallet.sparrow.net.Auth47;
|
import com.sparrowwallet.sparrow.net.Auth47;
|
||||||
import com.sparrowwallet.drongo.protocol.BlockHeader;
|
import com.sparrowwallet.drongo.protocol.BlockHeader;
|
||||||
|
@ -11,10 +20,6 @@ import com.sparrowwallet.drongo.protocol.ScriptType;
|
||||||
import com.sparrowwallet.drongo.protocol.Transaction;
|
import com.sparrowwallet.drongo.protocol.Transaction;
|
||||||
import com.sparrowwallet.drongo.psbt.PSBT;
|
import com.sparrowwallet.drongo.psbt.PSBT;
|
||||||
import com.sparrowwallet.drongo.uri.BitcoinURI;
|
import com.sparrowwallet.drongo.uri.BitcoinURI;
|
||||||
import com.sparrowwallet.drongo.wallet.BlockTransactionHashIndex;
|
|
||||||
import com.sparrowwallet.drongo.wallet.KeystoreSource;
|
|
||||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
|
||||||
import com.sparrowwallet.drongo.wallet.WalletTransaction;
|
|
||||||
import com.sparrowwallet.sparrow.control.TextUtils;
|
import com.sparrowwallet.sparrow.control.TextUtils;
|
||||||
import com.sparrowwallet.sparrow.control.TrayManager;
|
import com.sparrowwallet.sparrow.control.TrayManager;
|
||||||
import com.sparrowwallet.sparrow.event.*;
|
import com.sparrowwallet.sparrow.event.*;
|
||||||
|
@ -881,6 +886,8 @@ public class AppServices {
|
||||||
openBitcoinUri(uri);
|
openBitcoinUri(uri);
|
||||||
} else if(("auth47").equals(uri.getScheme())) {
|
} else if(("auth47").equals(uri.getScheme())) {
|
||||||
openAuth47Uri(uri);
|
openAuth47Uri(uri);
|
||||||
|
} else if(("lightning").equals(uri.getScheme())) {
|
||||||
|
openLnurlAuthUri(uri);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -903,11 +910,13 @@ public class AppServices {
|
||||||
private static void openBitcoinUri(URI uri) {
|
private static void openBitcoinUri(URI uri) {
|
||||||
try {
|
try {
|
||||||
BitcoinURI bitcoinURI = new BitcoinURI(uri.toString());
|
BitcoinURI bitcoinURI = new BitcoinURI(uri.toString());
|
||||||
Wallet wallet = selectWallet(null, null, "pay from");
|
List<PolicyType> policyTypes = Arrays.asList(PolicyType.values());
|
||||||
|
List<ScriptType> scriptTypes = Arrays.asList(ScriptType.ADDRESSABLE_TYPES);
|
||||||
|
Wallet wallet = selectWallet(policyTypes, scriptTypes, true, false, "pay from", false);
|
||||||
|
|
||||||
if(wallet != null) {
|
if(wallet != null) {
|
||||||
final Wallet sendingWallet = wallet;
|
final Wallet sendingWallet = wallet;
|
||||||
EventManager.get().post(new SendActionEvent(sendingWallet, new ArrayList<>(sendingWallet.getWalletUtxos().keySet())));
|
EventManager.get().post(new SendActionEvent(sendingWallet, new ArrayList<>(sendingWallet.getWalletUtxos().keySet()), true));
|
||||||
Platform.runLater(() -> EventManager.get().post(new SendPaymentsEvent(sendingWallet, List.of(bitcoinURI.toPayment()))));
|
Platform.runLater(() -> EventManager.get().post(new SendPaymentsEvent(sendingWallet, List.of(bitcoinURI.toPayment()))));
|
||||||
}
|
}
|
||||||
} catch(Exception e) {
|
} catch(Exception e) {
|
||||||
|
@ -915,31 +924,97 @@ public class AppServices {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void openAuth47Uri(URI uri) {
|
private static void openAuth47Uri(URI uri) {
|
||||||
try {
|
try {
|
||||||
Auth47 auth47 = new Auth47(uri);
|
Auth47 auth47 = new Auth47(uri);
|
||||||
Wallet wallet = selectWallet(null, Boolean.TRUE, "authenticate using your payment code");
|
List<ScriptType> scriptTypes = PaymentCode.SEGWIT_SCRIPT_TYPES;
|
||||||
|
Wallet wallet = selectWallet(List.of(PolicyType.SINGLE), scriptTypes, false, true, "login to " + auth47.getCallback().getHost(), true);
|
||||||
|
|
||||||
if(wallet != null) {
|
if(wallet != null) {
|
||||||
try {
|
try {
|
||||||
auth47.sendResponse(wallet);
|
auth47.sendResponse(wallet);
|
||||||
showSuccessDialog("Successful authentication", "Successfully authenticated to " + auth47.getCallback() + ".");
|
EventManager.get().post(new StatusEvent("Successfully authenticated to " + auth47.getCallback().getHost()));
|
||||||
} catch(Exception e) {
|
} catch(Exception e) {
|
||||||
log.error("Error authenticating auth47 URI", e);
|
log.error("Error authenticating auth47 URI", e);
|
||||||
showErrorDialog("Error authenticating", "Failed to authenticate.\n\n" + e.getMessage());
|
showErrorDialog("Error authenticating", "Failed to authenticate.\n\n" + e.getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch(Exception e) {
|
} catch(Exception e) {
|
||||||
|
log.error("Not a valid auth47 URI", e);
|
||||||
showErrorDialog("Not a valid auth47 URI", e.getMessage());
|
showErrorDialog("Not a valid auth47 URI", e.getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Wallet selectWallet(ScriptType scriptType, Boolean hasPaymentCode, String actionDescription) {
|
private static void openLnurlAuthUri(URI uri) {
|
||||||
|
try {
|
||||||
|
LnurlAuth lnurlAuth = new LnurlAuth(uri);
|
||||||
|
List<ScriptType> scriptTypes = ScriptType.getAddressableScriptTypes(PolicyType.SINGLE);
|
||||||
|
Wallet wallet = selectWallet(List.of(PolicyType.SINGLE), scriptTypes, true, true, lnurlAuth.getLoginMessage(), true);
|
||||||
|
|
||||||
|
if(wallet != null) {
|
||||||
|
if(wallet.isEncrypted()) {
|
||||||
|
Storage storage = AppServices.get().getOpenWallets().get(wallet);
|
||||||
|
Wallet copy = wallet.copy();
|
||||||
|
WalletPasswordDialog dlg = new WalletPasswordDialog(copy.getMasterName(), WalletPasswordDialog.PasswordRequirement.LOAD);
|
||||||
|
Optional<SecureString> password = dlg.showAndWait();
|
||||||
|
if(password.isPresent()) {
|
||||||
|
Storage.KeyDerivationService keyDerivationService = new Storage.KeyDerivationService(storage, password.get(), true);
|
||||||
|
keyDerivationService.setOnSucceeded(workerStateEvent -> {
|
||||||
|
EventManager.get().post(new StorageEvent(storage.getWalletId(wallet), TimedEvent.Action.END, "Done"));
|
||||||
|
ECKey encryptionFullKey = keyDerivationService.getValue();
|
||||||
|
Key key = new Key(encryptionFullKey.getPrivKeyBytes(), storage.getKeyDeriver().getSalt(), EncryptionType.Deriver.ARGON2);
|
||||||
|
copy.decrypt(key);
|
||||||
|
try {
|
||||||
|
lnurlAuth.sendResponse(copy);
|
||||||
|
EventManager.get().post(new StatusEvent("Successfully authenticated to " + lnurlAuth.getDomain()));
|
||||||
|
} catch(Exception e) {
|
||||||
|
showErrorDialog("Error authenticating", "Failed to authenticate.\n\n" + e.getMessage());
|
||||||
|
} finally {
|
||||||
|
key.clear();
|
||||||
|
encryptionFullKey.clear();
|
||||||
|
password.get().clear();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
keyDerivationService.setOnFailed(workerStateEvent -> {
|
||||||
|
EventManager.get().post(new StorageEvent(storage.getWalletId(wallet), TimedEvent.Action.END, "Failed"));
|
||||||
|
if(keyDerivationService.getException() instanceof InvalidPasswordException) {
|
||||||
|
Optional<ButtonType> optResponse = showErrorDialog("Invalid Password", "The wallet password was invalid. Try again?", ButtonType.CANCEL, ButtonType.OK);
|
||||||
|
if(optResponse.isPresent() && optResponse.get().equals(ButtonType.OK)) {
|
||||||
|
Platform.runLater(() -> openLnurlAuthUri(uri));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.error("Error deriving wallet key", keyDerivationService.getException());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
EventManager.get().post(new StorageEvent(storage.getWalletId(wallet), TimedEvent.Action.START, "Decrypting wallet..."));
|
||||||
|
keyDerivationService.start();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
lnurlAuth.sendResponse(wallet);
|
||||||
|
EventManager.get().post(new StatusEvent("Successfully authenticated to " + lnurlAuth.getDomain()));
|
||||||
|
} catch(LnurlAuth.LnurlAuthException e) {
|
||||||
|
showErrorDialog("Error authenticating", "Failed to authenticate.\n\n" + e.getMessage());
|
||||||
|
} catch(Exception e) {
|
||||||
|
log.error("Failed to authenticate using LNURL-auth", e);
|
||||||
|
showErrorDialog("Error authenticating", "Failed to authenticate.\n\n" + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch(Exception e) {
|
||||||
|
log.error("Not a valid LNURL-auth URI", e);
|
||||||
|
showErrorDialog("Not a valid LNURL-auth URI", e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Wallet selectWallet(List<PolicyType> policyTypes, List<ScriptType> scriptTypes, boolean taprootAllowed, boolean privateKeysRequired, String actionDescription, boolean alwaysAsk) {
|
||||||
Wallet wallet = null;
|
Wallet wallet = null;
|
||||||
List<Wallet> wallets = get().getOpenWallets().keySet().stream().filter(w -> (scriptType == null || w.getScriptType() == scriptType) && (hasPaymentCode == null || w.hasPaymentCode())).collect(Collectors.toList());
|
List<Wallet> wallets = get().getOpenWallets().keySet().stream().filter(w -> w.isValid() && policyTypes.contains(w.getPolicyType()) && scriptTypes.contains(w.getScriptType())
|
||||||
|
&& (!privateKeysRequired || w.getKeystores().stream().allMatch(Keystore::hasPrivateKey))).collect(Collectors.toList());
|
||||||
if(wallets.isEmpty()) {
|
if(wallets.isEmpty()) {
|
||||||
showErrorDialog("No wallet available", "Open a" + (hasPaymentCode == null ? "" : " software") + (scriptType == null ? "" : " " + scriptType.getDescription()) + " wallet to " + actionDescription + ".");
|
boolean taprootOpen = get().getOpenWallets().keySet().stream().anyMatch(w -> w.getScriptType() == ScriptType.P2TR);
|
||||||
} else if(wallets.size() == 1) {
|
showErrorDialog("No wallet available", "Open a" + (taprootOpen && !taprootAllowed ? " non-Taproot" : "") + (privateKeysRequired ? " software" : "") + " wallet to " + actionDescription + ".");
|
||||||
|
} else if(wallets.size() == 1 && !alwaysAsk) {
|
||||||
wallet = wallets.iterator().next();
|
wallet = wallets.iterator().next();
|
||||||
} else {
|
} else {
|
||||||
ChoiceDialog<Wallet> walletChoiceDialog = new ChoiceDialog<>(wallets.iterator().next(), wallets);
|
ChoiceDialog<Wallet> walletChoiceDialog = new ChoiceDialog<>(wallets.iterator().next(), wallets);
|
||||||
|
|
|
@ -8,10 +8,16 @@ import java.util.List;
|
||||||
|
|
||||||
public class SendActionEvent extends FunctionActionEvent {
|
public class SendActionEvent extends FunctionActionEvent {
|
||||||
private final List<BlockTransactionHashIndex> utxos;
|
private final List<BlockTransactionHashIndex> utxos;
|
||||||
|
private final boolean selectIfEmpty;
|
||||||
|
|
||||||
public SendActionEvent(Wallet wallet, List<BlockTransactionHashIndex> utxos) {
|
public SendActionEvent(Wallet wallet, List<BlockTransactionHashIndex> utxos) {
|
||||||
|
this(wallet, utxos, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public SendActionEvent(Wallet wallet, List<BlockTransactionHashIndex> utxos, boolean selectIfEmpty) {
|
||||||
super(Function.SEND, wallet);
|
super(Function.SEND, wallet);
|
||||||
this.utxos = utxos;
|
this.utxos = utxos;
|
||||||
|
this.selectIfEmpty = selectIfEmpty;
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<BlockTransactionHashIndex> getUtxos() {
|
public List<BlockTransactionHashIndex> getUtxos() {
|
||||||
|
@ -20,6 +26,6 @@ public class SendActionEvent extends FunctionActionEvent {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean selectFunction() {
|
public boolean selectFunction() {
|
||||||
return !getUtxos().isEmpty();
|
return selectIfEmpty || !getUtxos().isEmpty();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
204
src/main/java/com/sparrowwallet/sparrow/net/LnurlAuth.java
Normal file
204
src/main/java/com/sparrowwallet/sparrow/net/LnurlAuth.java
Normal file
|
@ -0,0 +1,204 @@
|
||||||
|
package com.sparrowwallet.sparrow.net;
|
||||||
|
|
||||||
|
import com.google.gson.JsonObject;
|
||||||
|
import com.google.gson.JsonParser;
|
||||||
|
import com.sparrowwallet.drongo.ExtendedKey;
|
||||||
|
import com.sparrowwallet.drongo.Utils;
|
||||||
|
import com.sparrowwallet.drongo.crypto.ChildNumber;
|
||||||
|
import com.sparrowwallet.drongo.crypto.ECDSASignature;
|
||||||
|
import com.sparrowwallet.drongo.crypto.ECKey;
|
||||||
|
import com.sparrowwallet.drongo.policy.PolicyType;
|
||||||
|
import com.sparrowwallet.drongo.protocol.Bech32;
|
||||||
|
import com.sparrowwallet.drongo.protocol.Sha256Hash;
|
||||||
|
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||||
|
import com.sparrowwallet.sparrow.AppServices;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import javax.crypto.Mac;
|
||||||
|
import javax.crypto.spec.SecretKeySpec;
|
||||||
|
import java.io.BufferedReader;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStreamReader;
|
||||||
|
import java.net.*;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.security.InvalidKeyException;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import java.util.stream.IntStream;
|
||||||
|
|
||||||
|
public class LnurlAuth {
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(LnurlAuth.class);
|
||||||
|
|
||||||
|
public static final ChildNumber LNURL_PURPOSE = new ChildNumber(138, true);
|
||||||
|
|
||||||
|
private final URL url;
|
||||||
|
private final byte[] k1;
|
||||||
|
private final String action;
|
||||||
|
|
||||||
|
public LnurlAuth(URI uri) throws MalformedURLException {
|
||||||
|
String lnurl = uri.getSchemeSpecificPart();
|
||||||
|
Bech32.Bech32Data bech32 = Bech32.decode(lnurl, 2000);
|
||||||
|
byte[] urlBytes = Bech32.convertBits(bech32.data, 0, bech32.data.length, 5, 8, false);
|
||||||
|
String strUrl = new String(urlBytes, StandardCharsets.UTF_8);
|
||||||
|
this.url = new URL(strUrl);
|
||||||
|
|
||||||
|
Map<String, String> parameterMap = new LinkedHashMap<>();
|
||||||
|
String query = url.getQuery();
|
||||||
|
if(query == null) {
|
||||||
|
throw new IllegalArgumentException("No k1 parameter provided.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if(query.startsWith("?")) {
|
||||||
|
query = query.substring(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
String[] pairs = query.split("&");
|
||||||
|
for(String pair : pairs) {
|
||||||
|
int idx = pair.indexOf("=");
|
||||||
|
if(idx < 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
parameterMap.put(pair.substring(0, idx), pair.substring(idx + 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
if(parameterMap.get("tag") == null || !parameterMap.get("tag").toLowerCase(Locale.ROOT).equals("login")) {
|
||||||
|
throw new IllegalArgumentException("Parameter tag was not set to login.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if(parameterMap.get("k1") == null || parameterMap.get("k1").length() != 64) {
|
||||||
|
throw new IllegalArgumentException("Parameter k1 was absent or of incorrect length.");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.k1 = Utils.hexToBytes(parameterMap.get("k1"));
|
||||||
|
} catch(Exception e) {
|
||||||
|
throw new IllegalArgumentException("Parameter k1 was not a valid hexadecimal value.");
|
||||||
|
}
|
||||||
|
|
||||||
|
this.action = parameterMap.get("action") == null ? "login" : parameterMap.get("action").toLowerCase(Locale.ROOT);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getDomain() {
|
||||||
|
return url.getHost();
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getLoginMessage() {
|
||||||
|
String domain = getDomain();
|
||||||
|
switch(action) {
|
||||||
|
case "register":
|
||||||
|
return "register an account on " + domain;
|
||||||
|
case "link":
|
||||||
|
return "link your existing account on " + domain;
|
||||||
|
case "auth":
|
||||||
|
return "authorise " + domain;
|
||||||
|
case "login":
|
||||||
|
default:
|
||||||
|
return "login to " + domain;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void sendResponse(Wallet wallet) throws LnurlAuthException, IOException {
|
||||||
|
URL callback = getReturnURL(wallet);
|
||||||
|
|
||||||
|
Proxy proxy = AppServices.getProxy();
|
||||||
|
if(proxy == null && callback.getHost().toLowerCase(Locale.ROOT).endsWith(TorService.TOR_ADDRESS_SUFFIX)) {
|
||||||
|
throw new LnurlAuthException("A Tor proxy must be configured to authenticate this resource.");
|
||||||
|
}
|
||||||
|
|
||||||
|
HttpURLConnection connection = proxy == null ? (HttpURLConnection) callback.openConnection() : (HttpURLConnection) callback.openConnection(proxy);
|
||||||
|
connection.setRequestMethod("GET");
|
||||||
|
connection.setRequestProperty("Content-Type", "application/json");
|
||||||
|
|
||||||
|
StringBuilder res = new StringBuilder();
|
||||||
|
try(BufferedReader br = new BufferedReader(new InputStreamReader(connection.getInputStream(), StandardCharsets.UTF_8))) {
|
||||||
|
String responseLine;
|
||||||
|
while((responseLine = br.readLine()) != null) {
|
||||||
|
res.append(responseLine.trim());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(log.isDebugEnabled()) {
|
||||||
|
log.debug("Received from " + callback + ": " + res);
|
||||||
|
}
|
||||||
|
|
||||||
|
JsonObject result = JsonParser.parseString(res.toString()).getAsJsonObject();
|
||||||
|
String status = result.get("status").getAsString();
|
||||||
|
if("OK".equals(status)) {
|
||||||
|
return;
|
||||||
|
} else if("ERROR".equals(status)) {
|
||||||
|
String reason = result.get("reason").getAsString();
|
||||||
|
throw new LnurlAuthException("Service returned error: " + reason);
|
||||||
|
} else {
|
||||||
|
throw new LnurlAuthException("Service returned unknown response: " + res);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private URL getReturnURL(Wallet wallet) {
|
||||||
|
try {
|
||||||
|
ECKey linkingKey = deriveLinkingKey(wallet);
|
||||||
|
byte[] signature = getSignature(linkingKey);
|
||||||
|
return new URL(url.toString() + "&sig=" + Utils.bytesToHex(signature) + "&key=" + Utils.bytesToHex(linkingKey.getPubKey()));
|
||||||
|
} catch(MalformedURLException e) {
|
||||||
|
throw new IllegalStateException("Malformed return URL", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private ECKey deriveLinkingKey(Wallet wallet) {
|
||||||
|
if(wallet.getPolicyType() != PolicyType.SINGLE) {
|
||||||
|
throw new IllegalArgumentException("Only singlesig wallets can authenticate.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if(wallet.isEncrypted()) {
|
||||||
|
throw new IllegalArgumentException("Wallet must be decrypted.");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
ExtendedKey masterPrivateKey = wallet.getKeystores().get(0).getExtendedMasterPrivateKey();
|
||||||
|
ECKey hashingKey = masterPrivateKey.getKey(List.of(LNURL_PURPOSE, ChildNumber.ZERO));
|
||||||
|
byte[] hash = getHmacSha256Hash(hashingKey.getPrivKeyBytes(), getDomain());
|
||||||
|
List<ChildNumber> pathIndexes = IntStream.range(0, 4).mapToLong(i -> ByteBuffer.wrap(hash, i * 4, 4).getInt() & 0xFFFFFFFFL)
|
||||||
|
.mapToObj(i -> new ChildNumber((int)i)).collect(Collectors.toList());
|
||||||
|
|
||||||
|
List<ChildNumber> derivationPath = new ArrayList<>();
|
||||||
|
derivationPath.add(LNURL_PURPOSE);
|
||||||
|
derivationPath.addAll(pathIndexes);
|
||||||
|
return masterPrivateKey.getKey(derivationPath);
|
||||||
|
} catch(Exception e) {
|
||||||
|
throw new IllegalStateException("Could not determine linking key", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte[] getSignature(ECKey linkingKey) {
|
||||||
|
ECDSASignature ecdsaSignature = linkingKey.signEcdsa(Sha256Hash.wrap(k1));
|
||||||
|
return ecdsaSignature.encodeToDER();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte[] getHmacSha256Hash(byte[] key, String data) throws NoSuchAlgorithmException, InvalidKeyException {
|
||||||
|
Mac sha256_HMAC = Mac.getInstance("HmacSHA256");
|
||||||
|
SecretKeySpec secret_key = new SecretKeySpec(key, "HmacSHA256");
|
||||||
|
sha256_HMAC.init(secret_key);
|
||||||
|
|
||||||
|
return sha256_HMAC.doFinal(data.getBytes(StandardCharsets.UTF_8));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final class LnurlAuthException extends Exception {
|
||||||
|
public LnurlAuthException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public LnurlAuthException(String message, Throwable cause) {
|
||||||
|
super(message, cause);
|
||||||
|
}
|
||||||
|
|
||||||
|
public LnurlAuthException(Throwable cause) {
|
||||||
|
super(cause);
|
||||||
|
}
|
||||||
|
|
||||||
|
public LnurlAuthException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
|
||||||
|
super(message, cause, enableSuppression, writableStackTrace);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -10,6 +10,9 @@ import org.slf4j.LoggerFactory;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
import static com.sparrowwallet.drongo.bip47.PaymentCode.SEGWIT_SCRIPT_TYPES;
|
||||||
|
import static com.sparrowwallet.drongo.bip47.PaymentCode.V1_SCRIPT_TYPES;
|
||||||
|
|
||||||
public class PayNym {
|
public class PayNym {
|
||||||
private static final Logger log = LoggerFactory.getLogger(PayNym.class);
|
private static final Logger log = LoggerFactory.getLogger(PayNym.class);
|
||||||
|
|
||||||
|
@ -68,11 +71,11 @@ public class PayNym {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static List<ScriptType> getSegwitScriptTypes() {
|
public static List<ScriptType> getSegwitScriptTypes() {
|
||||||
return List.of(ScriptType.P2PKH, ScriptType.P2SH_P2WPKH, ScriptType.P2WPKH);
|
return SEGWIT_SCRIPT_TYPES;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static List<ScriptType> getV1ScriptTypes() {
|
public static List<ScriptType> getV1ScriptTypes() {
|
||||||
return List.of(ScriptType.P2PKH);
|
return V1_SCRIPT_TYPES;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static PayNym fromString(String strPaymentCode, String nymId, String nymName, boolean segwit, List<PayNym> following, List<PayNym> followers) {
|
public static PayNym fromString(String strPaymentCode, String nymId, String nymName, boolean segwit, List<PayNym> following, List<PayNym> followers) {
|
||||||
|
|
Loading…
Reference in a new issue