mirror of
https://github.com/block/buzz.git
synced 2026-11-01 15:01:12 +00:00
Make pairing relay auth optional on mobile
Co-authored-by: npub12gtutshhh76rx0jx697f32f9tffd4hhp3hx58fp4x6u4uemkm7sqf8f757 <5217c5c2f7bfb4333e46d17c98a9255a52dadee18dcd43a43536b95e6776dfa0@sprout-oss.stage.blox.sqprod.co> Signed-off-by: npub12gtutshhh76rx0jx697f32f9tffd4hhp3hx58fp4x6u4uemkm7sqf8f757 <5217c5c2f7bfb4333e46d17c98a9255a52dadee18dcd43a43536b95e6776dfa0@sprout-oss.stage.blox.sqprod.co>
This commit is contained in:
parent
2c08271ba9
commit
a6c20eb50d
@@ -6,6 +6,9 @@ import 'package:web_socket_channel/web_socket_channel.dart';
|
||||
|
||||
import '../../shared/relay/nostr_models.dart';
|
||||
|
||||
const _desktopPairingAuthChallengeGrace = Duration(seconds: 3);
|
||||
const _pairingAuthOkTimeout = Duration(seconds: 5);
|
||||
|
||||
/// Ephemeral WebSocket connection for NIP-AB pairing.
|
||||
///
|
||||
/// Uses ephemeral keys for NIP-42 auth (not the stored user keys).
|
||||
@@ -35,7 +38,7 @@ class PairingSocket {
|
||||
|
||||
bool get isConnected => _connected;
|
||||
|
||||
/// Connect and authenticate via NIP-42.
|
||||
/// Connect to the pairing relay and answer NIP-42 auth when requested.
|
||||
Future<void> connect() async {
|
||||
try {
|
||||
_channel = WebSocketChannel.connect(Uri.parse(_wsUrl));
|
||||
@@ -59,23 +62,27 @@ class PairingSocket {
|
||||
},
|
||||
);
|
||||
|
||||
// Wait for auth with 8s timeout.
|
||||
_authTimeout = Timer(const Duration(seconds: 8), () {
|
||||
// Pairing relay auth is optional: the dedicated pairing relay is
|
||||
// intentionally authless, while main relays may still request NIP-42.
|
||||
// Match desktop's grace window: if no challenge arrives, proceed
|
||||
// unauthenticated; if one arrives, wait for the auth OK below.
|
||||
_authTimeout = Timer(_desktopPairingAuthChallengeGrace, () {
|
||||
if (_authCompleter != null && !_authCompleter!.isCompleted) {
|
||||
_authCompleter!.completeError(
|
||||
TimeoutException('NIP-42 auth timed out'),
|
||||
);
|
||||
_authCompleter!.complete();
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
await _authCompleter!.future;
|
||||
_authTimeout?.cancel();
|
||||
_authTimeout = null;
|
||||
_connected = true;
|
||||
} catch (e) {
|
||||
_authTimeout?.cancel();
|
||||
_authTimeout = null;
|
||||
await disconnect();
|
||||
_onDisconnected(e);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,6 +113,7 @@ class PairingSocket {
|
||||
_subscription?.cancel();
|
||||
_subscription = null;
|
||||
_authTimeout?.cancel();
|
||||
_authTimeout = null;
|
||||
final channel = _channel;
|
||||
_channel = null;
|
||||
if (channel != null) {
|
||||
@@ -155,6 +163,15 @@ class PairingSocket {
|
||||
if (data.length < 2) return;
|
||||
final challenge = data[1] as String;
|
||||
|
||||
// If a relay did challenge, keep waiting for the matching OK instead of
|
||||
// falling through as unauthenticated at the grace deadline.
|
||||
_authTimeout?.cancel();
|
||||
_authTimeout = Timer(_pairingAuthOkTimeout, () {
|
||||
if (_authCompleter != null && !_authCompleter!.isCompleted) {
|
||||
_failAuth(TimeoutException('NIP-42 auth OK timed out'));
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
// Build NIP-42 auth event (kind:22242) with ephemeral keys.
|
||||
final tags = <List<String>>[
|
||||
|
||||
91
mobile/test/features/pairing/pairing_socket_test.dart
Normal file
91
mobile/test/features/pairing/pairing_socket_test.dart
Normal file
@@ -0,0 +1,91 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:buzz/features/pairing/pairing_socket.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
void main() {
|
||||
test(
|
||||
'connect completes when pairing relay does not request auth',
|
||||
() async {
|
||||
final server = await HttpServer.bind(InternetAddress.loopbackIPv4, 0);
|
||||
addTearDown(server.close);
|
||||
|
||||
final sockets = <WebSocket>[];
|
||||
server.listen((request) async {
|
||||
final socket = await WebSocketTransformer.upgrade(request);
|
||||
sockets.add(socket);
|
||||
addTearDown(socket.close);
|
||||
await for (final _ in socket) {
|
||||
// This relay intentionally never sends AUTH.
|
||||
}
|
||||
});
|
||||
|
||||
final socket = PairingSocket(
|
||||
wsUrl: 'ws://${server.address.host}:${server.port}',
|
||||
ephemeralPrivkey: '1' * 64,
|
||||
onMessage: (_) {},
|
||||
onDisconnected: (_) {},
|
||||
);
|
||||
addTearDown(socket.dispose);
|
||||
|
||||
await socket.connect();
|
||||
|
||||
expect(socket.isConnected, isTrue);
|
||||
expect(sockets, hasLength(1));
|
||||
},
|
||||
timeout: const Timeout(Duration(seconds: 5)),
|
||||
);
|
||||
|
||||
test(
|
||||
'answers auth when relay requests NIP-42',
|
||||
() async {
|
||||
final server = await HttpServer.bind(InternetAddress.loopbackIPv4, 0);
|
||||
addTearDown(server.close);
|
||||
|
||||
final receivedAuth = Completer<List<dynamic>>();
|
||||
server.listen((request) async {
|
||||
final socket = await WebSocketTransformer.upgrade(request);
|
||||
addTearDown(socket.close);
|
||||
socket.add(jsonEncode(['AUTH', 'challenge']));
|
||||
await for (final raw in socket) {
|
||||
final message = jsonDecode(raw as String) as List<dynamic>;
|
||||
if (message.first == 'AUTH') {
|
||||
receivedAuth.complete(message);
|
||||
final event = message[1] as Map<String, dynamic>;
|
||||
socket.add(jsonEncode(['OK', event['id'], true, '']));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
final socket = PairingSocket(
|
||||
wsUrl: 'ws://${server.address.host}:${server.port}',
|
||||
ephemeralPrivkey: '1' * 64,
|
||||
onMessage: (_) {},
|
||||
onDisconnected: (_) {},
|
||||
);
|
||||
addTearDown(socket.dispose);
|
||||
|
||||
await socket.connect();
|
||||
|
||||
expect(socket.isConnected, isTrue);
|
||||
final auth = await receivedAuth.future;
|
||||
expect(auth.first, 'AUTH');
|
||||
final event = auth[1] as Map<String, dynamic>;
|
||||
expect(event['kind'], 22242);
|
||||
expect(
|
||||
event['tags'],
|
||||
contains(
|
||||
predicate<List<dynamic>>(
|
||||
(tag) =>
|
||||
tag.length == 2 &&
|
||||
tag[0] == 'challenge' &&
|
||||
tag[1] == 'challenge',
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
timeout: const Timeout(Duration(seconds: 5)),
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user