Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 9 additions & 3 deletions lib/app/features/auth/providers/auth_provider.m.dart
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import 'package:ion_identity_client/ion_identity.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';

part 'auth_provider.m.freezed.dart';

part 'auth_provider.m.g.dart';

@freezed
Expand Down Expand Up @@ -54,9 +55,14 @@ class Auth extends _$Auth {
final userLocalPasskeyCredsState =
currentIdentityKeyName != null ? localPasskeyCredsStates[currentIdentityKeyName] : null;
final eventSigner = currentIdentityKeyName != null
? await ref
.watch(ionConnectEventSignerProvider(currentIdentityKeyName).notifier)
.initEventSigner()
? await Future.wait([
ref
.watch(ed25519IonConnectEventSignerProvider(currentIdentityKeyName).notifier)
.initEventSigner(),
ref
.watch(secp256k1IonConnectEventSignerProvider(currentIdentityKeyName).notifier)
.initEventSigner(),
])
: null;

if (currentIdentityKeyName != null) {
Expand Down
15 changes: 15 additions & 0 deletions lib/app/features/ion_connect/model/signing_algorithm.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// SPDX-License-Identifier: ice License 1.0

/// Enum representing the available signing algorithms for Nostr events
enum SigningAlgorithm {
/// Ed25519 signing algorithm
ed25519('curve25519'),

/// secp256k1 Schnorr signing algorithm
secp256k1Schnorr('secp256k1');

const SigningAlgorithm(this.curveName);

/// The curve name associated with this signing algorithm
final String curveName;
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,73 +3,252 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:ion/app/features/auth/providers/auth_provider.m.dart';
import 'package:ion/app/features/ion_connect/ion_connect.dart';
import 'package:ion/app/features/ion_connect/model/signing_algorithm.dart';
import 'package:ion/app/features/ion_connect/providers/device_keypair_utils.dart';
import 'package:ion/app/services/ion_connect/ed25519_key_store.dart';
import 'package:ion/app/services/ion_connect/secp256k1_schnorr_key_store.dart';
import 'package:ion/app/services/storage/secure_storage.r.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';

part 'ion_connect_event_signer_provider.r.g.dart';

/// Abstract base class for IonConnect event signers
abstract class EventSignerService {
EventSignerService({
required this.identityKeyName,
});

final String identityKeyName;

/// Algorithm-specific storage key suffix
String get storageKeySuffix;

/// Create signer from private key
Future<EventSigner> createSignerFromPrivateKey(String privateKey);

/// Generate new signer
Future<EventSigner> generateSigner();

/// Check if signer initialization should be skipped (e.g., for device keypair restoration)
Future<bool> shouldSkipInitialization(Ref ref);

/// Get the storage key for this signer
String getStorageKey() => '${identityKeyName}_$storageKeySuffix';
}

/// Ed25519 implementation of IonConnectEventSigner
class Ed25519IonConnectEventSignerService extends EventSignerService {
Ed25519IonConnectEventSignerService({
required super.identityKeyName,
});

@override
String get storageKeySuffix => 'ion_connect_key_store';

@override
Future<EventSigner> createSignerFromPrivateKey(String privateKey) async {
return Ed25519KeyStore.fromPrivate(privateKey);
}

@override
Future<EventSigner> generateSigner() async {
return Ed25519KeyStore.generate();
}

@override
Future<bool> shouldSkipInitialization(Ref ref) async {
final deviceKeypairAttachment = await DeviceKeypairUtils.findDeviceKeypairAttachment(ref: ref);
// If there's an uploaded keypair, skip initialization and restore it later (LinkNewDevice)
return deviceKeypairAttachment != null;
}
}

/// Secp256k1 Schnorr implementation of IonConnectEventSigner
class Secp256k1IonConnectEventSignerService extends EventSignerService {
Secp256k1IonConnectEventSignerService({
required super.identityKeyName,
});

@override
String get storageKeySuffix => 'ion_connect_secp256k1_key_store';

@override
Future<EventSigner> createSignerFromPrivateKey(String privateKey) async {
return Secp256k1SchnorrKeyStore.fromPrivate(privateKey);
}

@override
Future<EventSigner> generateSigner() async {
return Secp256k1SchnorrKeyStore.generate();
}

@override
Future<bool> shouldSkipInitialization(Ref<Object?> ref) {
return Future.value(false);
}
}

@Riverpod(keepAlive: true)
class IonConnectEventSigner extends _$IonConnectEventSigner {
class Ed25519IonConnectEventSigner extends _$Ed25519IonConnectEventSigner {
late final EventSignerService _signer;

@override
Future<EventSigner?> build(String identityKeyName) async {
final storage = ref.watch(secureStorageProvider);
final storedKey = await storage.getString(key: _storageKey);
if (storedKey != null) {
return Ed25519KeyStore.fromPrivate(storedKey);
}
return null;
_signer = Ed25519IonConnectEventSignerService(identityKeyName: identityKeyName);
return _loadFromStorage();
}

Future<void> delete() async {
await ref.read(secureStorageProvider).remove(key: _storageKey);
await _deleteFromStorage();
}

Future<EventSigner?> initEventSigner() async {
final currentUserIonConnectEventSigner = await future;
if (currentUserIonConnectEventSigner != null) {
// Event signer already exists, reuse it
return currentUserIonConnectEventSigner;
final currentEventSigner = await future;
if (currentEventSigner != null) {
return currentEventSigner;
}

final deviceKeypairAttachment = await DeviceKeypairUtils.findDeviceKeypairAttachment(ref: ref);
if (deviceKeypairAttachment != null) {
// There's an uploaded keypair - return null and restore it later (LinkNewDevice)
if (await _signer.shouldSkipInitialization(ref)) {
return null;
}

// Generate a new event signer
return _generate();
}

/// Restores an event signer from a private key string
/// This is used for device keypair restoration from uploaded
Future<EventSigner> restoreFromPrivateKey(String privateKey) async {
final keyStore = await Ed25519KeyStore.fromPrivate(privateKey);
final keyStore = await _signer.createSignerFromPrivateKey(privateKey);
return _setEventSigner(keyStore);
}

Future<EventSigner> _generate() async {
final keyStore = await Ed25519KeyStore.generate();
return _setEventSigner(keyStore);
final signer = await _signer.generateSigner();
return _setEventSigner(signer);
}

Future<EventSigner> _setEventSigner(EventSigner signer) async {
await _saveToStorage(signer);
state = AsyncData(signer);
return signer;
}

Future<EventSigner?> _loadFromStorage() async {
final storage = ref.read(secureStorageProvider);
await storage.setString(key: _storageKey, value: signer.privateKey);
final storedKey = await storage.getString(key: _signer.getStorageKey());
if (storedKey != null) {
return _signer.createSignerFromPrivateKey(storedKey);
}
return null;
}

Future<void> _saveToStorage(EventSigner signer) async {
final storage = ref.read(secureStorageProvider);
await storage.setString(key: _signer.getStorageKey(), value: signer.privateKey);
}

Future<void> _deleteFromStorage() async {
final storage = ref.read(secureStorageProvider);
await storage.remove(key: _signer.getStorageKey());
}
}

@Riverpod(keepAlive: true)
class Secp256k1IonConnectEventSigner extends _$Secp256k1IonConnectEventSigner {
late final EventSignerService _signer;

@override
Future<EventSigner?> build(String identityKeyName) async {
_signer = Secp256k1IonConnectEventSignerService(identityKeyName: identityKeyName);
return _loadFromStorage();
}

Future<void> delete() async {
await _deleteFromStorage();
}

Future<EventSigner?> initEventSigner() async {
final currentEventSigner = await future;
if (currentEventSigner != null) {
return currentEventSigner;
}

return _generate();
}

Future<EventSigner> restoreFromPrivateKey(String privateKey) async {
final keyStore = await _signer.createSignerFromPrivateKey(privateKey);
return _setEventSigner(keyStore);
}

Future<EventSigner> _generate() async {
final signer = await _signer.generateSigner();
return _setEventSigner(signer);
}

Future<EventSigner> _setEventSigner(EventSigner signer) async {
await _saveToStorage(signer);
state = AsyncData(signer);
return signer;
}

String get _storageKey => '${identityKeyName}_ion_connect_key_store';
Future<EventSigner?> _loadFromStorage() async {
final storage = ref.read(secureStorageProvider);
final storedKey = await storage.getString(key: _signer.getStorageKey());
if (storedKey != null) {
return _signer.createSignerFromPrivateKey(storedKey);
}
return null;
}

Future<void> _saveToStorage(EventSigner signer) async {
final storage = ref.read(secureStorageProvider);
await storage.setString(key: _signer.getStorageKey(), value: signer.privateKey);
}

Future<void> _deleteFromStorage() async {
final storage = ref.read(secureStorageProvider);
await storage.remove(key: _signer.getStorageKey());
}
}

@Riverpod(keepAlive: true)
Future<EventSigner?> currentUserIonConnectEventSigner(Ref ref) async {
Future<EventSigner?> currentUserEventSigner(Ref ref, SigningAlgorithm algorithm) async {
final currentIdentityKeyName = ref.watch(currentIdentityKeyNameSelectorProvider);
if (currentIdentityKeyName == null) {
return null;
}
return ref.watch(ionConnectEventSignerProvider(currentIdentityKeyName).future);

// Get the signer from the algorithm-specific provider
final eventSigner = switch (algorithm) {
SigningAlgorithm.ed25519 =>
await ref.watch(ed25519IonConnectEventSignerProvider(currentIdentityKeyName).future),
SigningAlgorithm.secp256k1Schnorr =>
await ref.watch(secp256k1IonConnectEventSignerProvider(currentIdentityKeyName).future),
};

// If signer doesn't exist, initialize it
if (eventSigner == null) {
return switch (algorithm) {
SigningAlgorithm.ed25519 => await ref
.read(ed25519IonConnectEventSignerProvider(currentIdentityKeyName).notifier)
.initEventSigner(),
SigningAlgorithm.secp256k1Schnorr => await ref
.read(secp256k1IonConnectEventSignerProvider(currentIdentityKeyName).notifier)
.initEventSigner(),
};
}

return eventSigner;
}

// Backward compatibility - keep the old name
@Riverpod(keepAlive: true)
Future<EventSigner?> currentUserIonConnectEventSigner(Ref ref) async {
return ref.watch(currentUserEventSignerProvider(SigningAlgorithm.ed25519).future);
}

// Backward compatibility - keep the old name
@Riverpod(keepAlive: true)
Future<EventSigner?> ionConnectEventSigner(Ref ref, String identityKeyName) async {
final eventSigner = await ref.watch(ed25519IonConnectEventSignerProvider(identityKeyName).future);
return eventSigner;
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import 'package:ion/app/features/ion_connect/model/events_metadata_builder.dart'
import 'package:ion/app/features/ion_connect/model/file_metadata.f.dart';
import 'package:ion/app/features/ion_connect/model/ion_connect_entity.dart';
import 'package:ion/app/features/ion_connect/model/ion_connect_gift_wrap.f.dart';
import 'package:ion/app/features/ion_connect/model/signing_algorithm.dart';
import 'package:ion/app/features/ion_connect/providers/ion_connect_cache.r.dart';
import 'package:ion/app/features/ion_connect/providers/ion_connect_event_parser.r.dart';
import 'package:ion/app/features/ion_connect/providers/ion_connect_event_signer_provider.r.dart';
Expand Down Expand Up @@ -394,21 +395,31 @@ class IonConnectNotifier extends _$IonConnectNotifier {
Future<EventMessage> sign(
EventSerializable entityData, {
bool includeMasterPubkey = true,
SigningAlgorithm algorithm = SigningAlgorithm.ed25519,
}) async {
final mainWallet = await ref.read(mainWalletProvider.future);

if (mainWallet == null) {
throw MainWalletNotFoundException();
}

final eventSigner = await ref.read(currentUserIonConnectEventSignerProvider.future);

// Get the appropriate signer based on the algorithm
// The provider handles initialization automatically
// Ed25519 will have prefix (default behavior), Secp256k1Schnorr won't have prefix
final eventSigner = await ref.read(currentUserEventSignerProvider(algorithm).future);
if (eventSigner == null) {
throw EventSignerNotFoundException();
}

// Only use createdAtSeconds for secp256k1Schnorr (Nostr protocol expects seconds)
// For ed25519, use default (null will use current time in microseconds)
final createdAt = algorithm == SigningAlgorithm.secp256k1Schnorr
? DateTime.now().millisecondsSinceEpoch ~/ 1000
: null;

return entityData.toEventMessage(
eventSigner,
createdAt: createdAt,
tags: [
if (includeMasterPubkey) MasterPubkeyTag(value: mainWallet.signingKey.publicKey).toTag(),
],
Expand Down Expand Up @@ -436,9 +447,16 @@ class IonConnectNotifier extends _$IonConnectNotifier {
final createdAt = DateTime.now();
final masterPubkey = mainWallet.signingKey.publicKey;

// Only use createdAtSeconds for secp256k1 (Nostr protocol expects seconds)
// For ed25519, use microseconds (default behavior)
final createdAtTimestamp =
mainWallet.signingKey.curve.toLowerCase() == SigningAlgorithm.secp256k1Schnorr.curveName
? createdAt.millisecondsSinceEpoch ~/ 1000
: createdAt.microsecondsSinceEpoch;

final eventId = EventMessage.calculateEventId(
publicKey: masterPubkey,
createdAt: createdAt.microsecondsSinceEpoch,
createdAt: createdAtTimestamp,
kind: kind,
tags: tags,
content: '',
Expand All @@ -464,7 +482,7 @@ class IonConnectNotifier extends _$IonConnectNotifier {
return EventMessage(
id: eventId,
pubkey: masterPubkey,
createdAt: createdAt.microsecondsSinceEpoch,
createdAt: createdAtTimestamp,
kind: kind,
tags: tags,
content: '',
Expand Down
Loading