Files
nowser/lib/provider/remote_signing_provider.dart
2025-11-19 19:56:11 +08:00

488 lines
17 KiB
Dart

import 'dart:convert';
import 'dart:developer';
import 'package:bot_toast/bot_toast.dart';
import 'package:flutter/material.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:nostr_sdk/client_utils/keys.dart';
import 'package:nostr_sdk/event.dart';
import 'package:nostr_sdk/event_kind.dart';
import 'package:nostr_sdk/filter.dart';
import 'package:nostr_sdk/nip46/nostr_remote_request.dart';
import 'package:nostr_sdk/nip46/nostr_remote_response.dart';
import 'package:nostr_sdk/relay/client_connected.dart';
import 'package:nostr_sdk/relay/relay.dart';
import 'package:nostr_sdk/relay/relay_isolate.dart';
import 'package:nostr_sdk/relay/relay_status.dart';
import 'package:nostr_sdk/signer/local_nostr_signer.dart';
import 'package:nostr_sdk/signer/nostr_signer.dart';
import 'package:nostr_sdk/utils/hash_util.dart';
import 'package:nostr_sdk/utils/string_util.dart';
import 'package:nowser/const/app_type.dart';
import 'package:nowser/const/auth_type.dart';
import 'package:nowser/data/app.dart';
import 'package:nowser/data/nostrconnect_remote_signing_info.dart';
import 'package:nowser/provider/permission_check_mixin.dart';
import 'package:relay_sdk/network/memory/mem_relay_client.dart';
import '../component/auth_dialog/auth_app_connect_dialog.dart';
import '../data/remote_signing_info.dart';
import '../data/remote_signing_info_db.dart';
import '../main.dart';
import 'build_in_relay_provider.dart';
class RemoteSigningProvider extends ChangeNotifier with PermissionCheckMixin {
BuildContext? context;
void updateContext(BuildContext _context) {
context = _context;
}
// remoteSignerPubkey - Relay
Map<String, List<Relay>> relayMap = {};
// remoteSignerPubkey - RemoteSigningInfo
Map<String, RemoteSigningInfo> remoteSigningInfoMap = {};
// remoteSignerPubkey - App
Map<String, App> appMap = {};
Future<void> init() async {
encryptKey = await getOrGenEncryptKey();
}
Future<void> reload() async {
relayMap = {};
remoteSigningInfoMap = {};
appMap = {};
load();
}
var encryptKey = "";
Future<void> load() async {
var remoteAppList = appProvider.remoteAppList();
for (var remoteApp in remoteAppList) {
await addRemoteApp(remoteApp, encryptKey);
}
}
static const String KEY_NAME = "remoteKey";
Future<String> getOrGenEncryptKey() async {
const storage = FlutterSecureStorage();
var str = await storage.read(key: KEY_NAME);
if (StringUtil.isBlank(str)) {
str = HashUtil.md5(StringUtil.rndNameStr(10));
await storage.write(key: KEY_NAME, value: str);
}
return str!;
}
Future<List<Relay>?> addRemoteApp(App remoteApp, String encryptKey) async {
var remoteSigningInfo =
await RemoteSigningInfoDB.getByAppId(remoteApp.id!, encryptKey);
if (remoteSigningInfo != null &&
StringUtil.isNotBlank(remoteSigningInfo.remoteSignerKey) &&
StringUtil.isNotBlank(remoteSigningInfo.remotePubkey) &&
StringUtil.isNotBlank(remoteSigningInfo.localPubkey) &&
StringUtil.isNotBlank(remoteSigningInfo.relays)) {
var relays = connectToRelay(remoteSigningInfo);
var remoteSignerPubkey = getPublicKey(remoteSigningInfo.remoteSignerKey!);
remoteSigningInfoMap[remoteSignerPubkey] = remoteSigningInfo;
appMap[remoteSignerPubkey] = remoteApp;
relayMap[remoteSignerPubkey] = relays;
return relays;
}
}
List<Relay> connectToRelay(RemoteSigningInfo remoteSigningInfo) {
var remoteSignerPubkey = getPublicKey(remoteSigningInfo.remoteSignerKey!);
var relayAddrs = remoteSigningInfo.relays!.split(",");
List<Relay> relays = [];
for (var relayAddr in relayAddrs) {
bool isLocalRelay = false;
Relay? relay;
var relayStatus = RelayStatus(remoteSignerPubkey);
if (relayAddr == "ws://localhost:${BuildInRelayProvider.port}" ||
relayAddr == "ws://127.0.0.1:${BuildInRelayProvider.port}") {
// use pubkey relace with
relay = MemRelayClient(relayAddr, relayStatus);
isLocalRelay = true;
} else {
// use pubkey relace with
relay = RelayIsolate(relayAddr, relayStatus);
}
var filter = Filter(
p: [remoteSignerPubkey],
since: DateTime.now().millisecondsSinceEpoch ~/ 1000);
relay.pendingAuthedMessages
.add(["REQ", StringUtil.rndNameStr(10), filter.toJson()]);
relay.pendingMessages
.add(["REQ", StringUtil.rndNameStr(10), filter.toJson()]);
relay.onMessage = _onEvent;
if (isLocalRelay) {
buildInRelayProvider.addMemClient(relay as MemRelayClient);
} else {
relay.connect();
}
relays.add(relay);
}
return relays;
}
Future<void> onRequest(
List<Relay> relays,
NostrRemoteRequest request,
RemoteSigningInfo remoteSigningInfo,
String localPubkey,
App? app,
) async {
NostrSigner signerSigner =
LocalNostrSigner(remoteSigningInfo.remoteSignerKey!);
var remoteSignerPubkey = getPublicKey(remoteSigningInfo.remoteSignerKey!);
var appType = AppType.REMOTE;
var code = remoteSignerPubkey;
NostrRemoteResponse? response;
if (request.method == "ping") {
response = NostrRemoteResponse(request.id, "pong");
sendResponse(
relays, response, signerSigner, localPubkey, remoteSignerPubkey);
} else if (request.method == "connect") {
if (app != null) {
response = NostrRemoteResponse(request.id, "ack");
} else {
if (request.params.length <= 1) {
response = NostrRemoteResponse(request.id, "", error: "params error");
} else {
if (request.params[0] == remoteSignerPubkey &&
request.params[1] == remoteSigningInfo.secret) {
// check pass, init app
var newApp = await getApp(appType, code);
await AuthAppConnectDialog.show(context!, newApp);
// reload from provider
app = appProvider.getApp(appType, code);
if (app == null) {
response =
NostrRemoteResponse(request.id, "", error: "connect fail");
} else {
remoteSigningInfo.appId = app.id;
remoteSigningInfo.localPubkey = localPubkey;
remoteSigningInfo.updatedAt =
DateTime.now().millisecondsSinceEpoch ~/ 1000;
RemoteSigningInfoDB.update(remoteSigningInfo, encryptKey);
remoteSigningInfoMap[remoteSignerPubkey] = remoteSigningInfo;
appMap[remoteSignerPubkey] = app;
_penddingRemoteApps.removeWhere((rsi) {
if (rsi.id == remoteSigningInfo.id) {
return true;
}
return false;
});
response = NostrRemoteResponse(request.id, "ack");
}
} else {
response = NostrRemoteResponse(request.id, "",
error: "connect check fail");
}
}
}
if (response != null) {
sendResponse(
relays, response, signerSigner, localPubkey, remoteSignerPubkey);
}
} else {
if (remoteSigningInfo.localPubkey != localPubkey) {
// Remote signing should connect first.
response = NostrRemoteResponse(request.id, "",
error: "Local pubkey not allow.");
sendResponse(
relays, response, signerSigner, localPubkey, remoteSignerPubkey);
return;
}
if (app == null) {
// Remote signing should connect first.
response = NostrRemoteResponse(request.id, "",
error: "Remote signing should connect first.");
sendResponse(
relays, response, signerSigner, localPubkey, remoteSignerPubkey);
return;
}
int? eventKind;
String? authDetail;
String? thirdPartyPubkey;
int authType = AuthType.GET_PUBLIC_KEY;
dynamic eventObj;
if (request.method == "sign_event") {
authType = AuthType.SIGN_EVENT;
authDetail = request.params[0];
eventObj = jsonDecode(authDetail);
eventKind = eventObj["kind"];
} else if (request.method == "get_relays") {
authType = AuthType.GET_RELAYS;
} else if (request.method == "get_public_key") {
authType = AuthType.GET_PUBLIC_KEY;
} else if (request.method == "nip04_encrypt") {
authType = AuthType.NIP04_ENCRYPT;
thirdPartyPubkey = request.params[0];
authDetail = request.params[1];
} else if (request.method == "nip04_decrypt") {
authType = AuthType.NIP04_DECRYPT;
thirdPartyPubkey = request.params[0];
authDetail = request.params[1];
} else if (request.method == "nip44_encrypt") {
authType = AuthType.NIP44_ENCRYPT;
thirdPartyPubkey = request.params[0];
authDetail = request.params[1];
} else if (request.method == "nip44_decrypt") {
authType = AuthType.NIP44_DECRYPT;
thirdPartyPubkey = request.params[0];
authDetail = request.params[1];
}
checkPermission(context!, appType, code, authType,
eventKind: eventKind, authDetail: authDetail, (app, rejectType) {
response = NostrRemoteResponse(request.id, "", error: "forbid");
sendResponse(
relays, response, signerSigner, localPubkey, remoteSignerPubkey);
}, (app, signer) async {
if (request.method == "sign_event") {
var tags = eventObj["tags"];
Event? event = Event(
app.pubkey!, eventObj["kind"], tags ?? [], eventObj["content"],
createdAt: eventObj["created_at"]);
log(jsonEncode(event.toJson()));
event = await signer.signEvent(event);
if (event == null) {
log("sign event fail");
return;
}
response =
NostrRemoteResponse(request.id, jsonEncode(event.toJson()));
} else if (request.method == "get_relays") {
// TODO
} else if (request.method == "get_public_key") {
var pubkey = await signer.getPublicKey();
response = NostrRemoteResponse(request.id, pubkey!);
} else if (request.method == "nip04_encrypt") {
var text = await signer.encrypt(thirdPartyPubkey, authDetail);
response = NostrRemoteResponse(request.id, text!);
} else if (request.method == "nip04_decrypt") {
var text = await signer.decrypt(thirdPartyPubkey, authDetail);
response = NostrRemoteResponse(request.id, text!);
} else if (request.method == "nip44_encrypt") {
var text = await signer.nip44Encrypt(thirdPartyPubkey, authDetail);
response = NostrRemoteResponse(request.id, text!);
} else if (request.method == "nip44_decrypt") {
var text = await signer.nip44Decrypt(thirdPartyPubkey, authDetail);
response = NostrRemoteResponse(request.id, text!);
}
sendResponse(
relays, response, signerSigner, localPubkey, remoteSignerPubkey);
});
}
}
Future<void> sendResponse(List<Relay> relays, NostrRemoteResponse? response,
NostrSigner signer, String localPubkey, String remoteSignerPubkey) async {
if (response != null) {
// print("response:");
// print(response.toString());
var result = await response.encrypt(signer, localPubkey);
Event? event = Event(
remoteSignerPubkey,
EventKind.NOSTR_REMOTE_SIGNING,
[
["p", localPubkey]
],
result!);
event = await signer.signEvent(event);
if (event != null) {
for (var relay in relays) {
if (relay.relayStatus.connected == ClientConneccted.CONNECTED) {
relay.send(["EVENT", event.toJson()]);
} else {
relay.pendingAuthedMessages.add(["EVENT", event.toJson()]);
relay.pendingMessages.add(["EVENT", event.toJson()]);
}
}
}
}
}
Map<String, int> handledIds = {};
Future<void> _onEvent(Relay relay, List<dynamic> json) async {
// print("request");
// print(json);
var remoteSignerPubkey = relay.relayStatus.addr;
var remoteSigningInfo = remoteSigningInfoMap[remoteSignerPubkey];
if (remoteSigningInfo == null) {
print("remoteSigningInfo is null");
return;
}
var remoteSigner = LocalNostrSigner(remoteSigningInfo.remoteSignerKey!);
var signer = await keyProvider.getSigner(remoteSigningInfo.remotePubkey!);
if (signer == null) {
return;
}
final messageType = json[0];
if (messageType == 'EVENT') {
try {
final event = Event.fromJson(json[2]);
// add some statistics
relay.relayStatus.noteReceive();
event.sources.add(relay.url);
if (event.kind == EventKind.NOSTR_REMOTE_SIGNING) {
if (handledIds[event.id] == null) {
var request = await NostrRemoteRequest.decrypt(
event.content, remoteSigner, event.pubkey);
var relays = relayMap[remoteSignerPubkey];
if (relays == null || relays.isEmpty) {
relays = [relay];
}
if (request != null) {
onRequest(relays, request, remoteSigningInfo, event.pubkey,
appMap[remoteSignerPubkey]);
}
handledIds[event.id] = 1;
}
}
} catch (err) {
log(err.toString());
}
} else if (messageType == 'EOSE') {
} else if (messageType == "NOTICE") {
} else if (messageType == "AUTH") {
// auth needed
if (json.length < 2) {
log("AUTH result not right.");
return;
}
final challenge = json[1] as String;
var tags = [
["relay", relay.url],
["challenge", challenge]
];
Event? event =
Event(remoteSignerPubkey, EventKind.AUTHENTICATION, tags, "");
event = await remoteSigner.signEvent(event);
if (event != null) {
relay.send(["AUTH", event.toJson()], forceSend: true);
relay.relayStatus.authed = true;
if (relay.pendingAuthedMessages.isNotEmpty) {
Future.delayed(const Duration(seconds: 1), () {
for (var message in relay.pendingAuthedMessages) {
relay.send(message);
}
relay.pendingAuthedMessages.clear();
});
}
}
}
}
Future<bool> addNostrconnectRemoteSigningInfo(
NostrconnectRemoteSigningInfo remoteSigningInfo) async {
NostrSigner signerSigner =
LocalNostrSigner(remoteSigningInfo.remoteSignerKey!);
var remoteSignerPubkey = getPublicKey(remoteSigningInfo.remoteSignerKey!);
var appType = AppType.REMOTE;
var code = remoteSignerPubkey;
App? app = await getApp(appType, code);
app.name = remoteSigningInfo.name;
if (StringUtil.isBlank(app.name)) {
app.name = remoteSigningInfo.url;
}
app.pubkey = remoteSigningInfo.remotePubkey;
app.image = remoteSigningInfo.image;
await AuthAppConnectDialog.show(context!, app);
app = appProvider.getApp(appType, code);
if (app == null) {
BotToast.showText(text: "Remote App connect fail.");
return false;
}
remoteSigningInfo.appId = app.id;
remoteSigningInfo.updatedAt = DateTime.now().millisecondsSinceEpoch ~/ 1000;
await RemoteSigningInfoDB.insert(remoteSigningInfo, encryptKey);
var relays = await addRemoteApp(app, encryptKey);
if (relays != null) {
var response = NostrRemoteResponse(
StringUtil.rndNameStr(12), remoteSigningInfo.secret!);
await sendResponse(relays, response, signerSigner,
remoteSigningInfo.localPubkey!, remoteSignerPubkey);
}
return true;
}
List<RemoteSigningInfo> _penddingRemoteApps = [];
List<RemoteSigningInfo> get penddingRemoteApps => _penddingRemoteApps;
Future<void> saveBunkerRemoteSigningInfo(
RemoteSigningInfo remoteSigningInfo) async {
await RemoteSigningInfoDB.insert(remoteSigningInfo, encryptKey);
await reloadPenddingRemoteApps();
notifyListeners();
}
Future<void> reloadPenddingRemoteApps() async {
var list = await RemoteSigningInfoDB.penddingRemoteSigningInfo(encryptKey);
_penddingRemoteApps = list;
notifyListeners();
connectPenddingRemoteApp();
}
void connectPenddingRemoteApp() {
for (var remoteSigningInfo in _penddingRemoteApps) {
if (StringUtil.isNotBlank(remoteSigningInfo.remoteSignerKey) &&
StringUtil.isNotBlank(remoteSigningInfo.remotePubkey) &&
StringUtil.isNotBlank(remoteSigningInfo.relays)) {
var remoteSignerPubkey =
getPublicKey(remoteSigningInfo.remoteSignerKey!);
if (remoteSigningInfoMap[remoteSignerPubkey] != null) {
continue;
}
remoteSigningInfoMap[remoteSignerPubkey] = remoteSigningInfo;
var relays = connectToRelay(remoteSigningInfo);
relayMap[remoteSignerPubkey] = relays;
}
}
}
}