Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Alpha preview #31

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
Open
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
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -70,4 +70,5 @@ chrome/.packages
# Misc
*.log
*.lock
.pdm
.pdm
bfg-1.14.0.jar
2 changes: 1 addition & 1 deletion android/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ plugins {
android {
namespace = "com.example.mostro_mobile"
compileSdk = flutter.compileSdkVersion
ndkVersion = "25.1.8937393" // flutter.ndkVersion
ndkVersion = "25.1.8937393" //flutter.ndkVersion

compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
Expand Down
3 changes: 3 additions & 0 deletions l10n.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
arb-dir: lib/l10n
template-arb-file: intl_en.arb
output-dir: lib/generated
13 changes: 6 additions & 7 deletions lib/core/config.dart
Original file line number Diff line number Diff line change
@@ -1,20 +1,19 @@
// lib/core/config.dart

import 'package:flutter/foundation.dart';

class Config {
// Configuración de Nostr
static const List<String> nostrRelays = [
'ws://127.0.0.1:7000',
//'ws://10.0.2.2:7000',
'wss://relay.damus.io',
'wss://relay.mostro.network',
'wss://relay.nostr.net',
//'wss://relay.damus.io',
//'wss://relay.mostro.network',
//'wss://relay.nostr.net',
// Agrega más relays aquí si es necesario
];

// Npub de Mostro
// hexkey de Mostro
static const String mostroPubKey =
'npub1n5yrh6lkvc0l3lcmcfwake4r3ex7jrm0e6lumsc22d8ylf7jwk0qack9tql;';
'9d9d0455a96871f2dc4289b8312429db2e925f167b37c77bf7b28014be235980';

// Tiempo de espera para conexiones a relays
static const Duration nostrConnectionTimeout = Duration(seconds: 30);
Expand Down
31 changes: 14 additions & 17 deletions lib/core/utils/auth_utils.dart
Original file line number Diff line number Diff line change
@@ -1,33 +1,30 @@
import 'package:flutter_secure_storage/flutter_secure_storage.dart';

class AuthUtils {
static const _storage = FlutterSecureStorage();

static Future<void> savePrivateKeyAndPin(String privateKey, String pin) async {
await _storage.write(key: 'user_private_key', value: privateKey);
await _storage.write(key: 'user_pin', value: pin);
}
/// Temporary implementation for alpha preview.
/// WARNING: This is not secure and should not be used in production.
/// TODO: Implement secure storage for credentials
static Future<void> savePrivateKeyAndPin(
String privateKey, String pin) async {}

/// Temporary implementation for alpha preview.
/// WARNING: This always returns null and should not be used in production.
/// TODO: Implement secure key retrieval
static Future<String?> getPrivateKey() async {
return await _storage.read(key: 'user_private_key');
return null;
}

static Future<bool> verifyPin(String inputPin) async {
final storedPin = await _storage.read(key: 'user_pin');
return storedPin == inputPin;
throw UnimplementedError('verifyPin is not implemented yet');
Comment on lines 15 to +16
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Standardize error handling approach

The error messages are inconsistent with the alpha preview nature of the app. Consider using a custom exception type that clearly indicates the preview status.

Create and use a custom exception:

+ /// Exception thrown when attempting to use unimplemented preview features
+ class PreviewFeatureException implements Exception {
+   final String feature;
+   PreviewFeatureException(this.feature);
+   
+   @override
+   String toString() => 
+     'Feature "$feature" is not implemented in this preview version';
+ }

  static Future<bool> verifyPin(String inputPin) async {
-   throw UnimplementedError('verifyPin is not implemented yet');
+   throw PreviewFeatureException('PIN verification');
  }

Also applies to: 13-14, 19-20

}

static Future<void> deleteCredentials() async {
await _storage.delete(key: 'user_private_key');
await _storage.delete(key: 'user_pin');
await _storage.delete(key: 'use_biometrics');
throw UnimplementedError('deleteCredentials is not implemented yet');
Comment on lines 18 to +20
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codebase verification

Add error handling for unimplemented credential management

The code clearly indicates this is an alpha preview with appropriate warnings in comments, but the UnimplementedError exceptions need proper error handling in the UI. Currently, there are no UI messages specifically handling these unimplemented features, which could lead to unexpected crashes.

  • lib/core/utils/auth_utils.dart: All authentication methods throw UnimplementedError
  • lib/presentation/auth/screens/login_screen.dart and register_screen.dart: Have error snackbars but no specific handling for unimplemented features
🔗 Analysis chain

Verify user feedback for unimplemented credential management

While the implementation is appropriate for an alpha preview, ensure the UI properly communicates this limitation to users.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Search for UI elements or strings related to credential deletion
rg -t dart "delete.*credentials?" lib/
rg -t dart "credentials.*delete?" lib/

Length of output: 81


Script:

#!/bin/bash
# Search for UI components and strings that might reference credential management
rg -t dart "credentials?" -A 3 -B 3 lib/

Length of output: 543


Script:

#!/bin/bash
# Look for any UI dialog, snackbar, or error messages
rg -t dart "(showDialog|SnackBar|showError|message.*credentials)" lib/

Length of output: 694


Script:

#!/bin/bash
# Check for any auth-related UI components
fd -e dart . lib/ --exec grep -l "auth"

Length of output: 442


Script:

#!/bin/bash
# Check the auth utils implementation for any alpha/preview warnings
cat lib/core/utils/auth_utils.dart

Length of output: 1084


Script:

#!/bin/bash
# Look for any alpha/preview related messages in auth screens
rg -t dart "alpha|preview|not.*implement" lib/presentation/auth/

Length of output: 66

}

static Future<void> enableBiometrics() async {
await _storage.write(key: 'use_biometrics', value: 'true');
throw UnimplementedError('enableBiometrics is not implemented yet');
}

static Future<bool> isBiometricsEnabled() async {
return await _storage.read(key: 'use_biometrics') == 'true';
throw UnimplementedError('isBiometricsEnabled is not implemented yet');
}
}
}
1 change: 0 additions & 1 deletion lib/core/utils/biometrics_helper.dart
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ class BiometricsHelper {
),
);
} catch (e) {
print(e);
return false;
}
}
Expand Down
232 changes: 162 additions & 70 deletions lib/core/utils/nostr_utils.dart
Original file line number Diff line number Diff line change
@@ -1,15 +1,24 @@
import 'dart:convert';
import 'dart:typed_data';
import 'dart:math';
import 'package:crypto/crypto.dart';
import 'package:dart_nostr/dart_nostr.dart';
import 'package:encrypt/encrypt.dart' as encrypt;
import 'package:elliptic/elliptic.dart';
import 'package:nip44/nip44.dart';

class NostrUtils {
static final Nostr _instance = Nostr.instance;

// Generación de claves
static NostrKeyPairs generateKeyPair() {
return _instance.keysService.generateKeyPair();
try {
final privateKey = generatePrivateKey();
if (!isValidPrivateKey(privateKey)) {
throw Exception('Generated invalid private key');
}
return NostrKeyPairs(private: privateKey);
} catch (e) {
throw Exception('Failed to generate key pair: $e');
}
}

static NostrKeyPairs generateKeyPairFromPrivateKey(String privateKey) {
Expand All @@ -18,7 +27,11 @@ class NostrUtils {
}

static String generatePrivateKey() {
return _instance.keysService.generatePrivateKey();
try {
return getS256().generatePrivateKey().toHex();
} catch (e) {
throw Exception('Failed to generate private key: $e');
}
}

// Codificación y decodificación de claves
Expand Down Expand Up @@ -117,96 +130,175 @@ class NostrUtils {
return digest.toString(); // Devuelve el ID como una cadena hex
}

// NIP-59 y NIP-44 funciones
static NostrEvent createNIP59Event(
String content, String recipientPubKey, String senderPrivateKey) {
final senderKeyPair = generateKeyPairFromPrivateKey(senderPrivateKey);
final sharedSecret =
_calculateSharedSecret(senderPrivateKey, recipientPubKey);
/// Generates a timestamp between now and 48 hours ago to enhance privacy
/// by decorrelating event timing from creation time.
/// @throws if system clock is ahead of network time
static DateTime randomNow() {
final now = DateTime.now();
// Validate system time isn't ahead
final networkTime = DateTime.now().toUtc();
if (now.isAfter(networkTime.add(Duration(minutes: 5)))) {
throw Exception('System clock is ahead of network time');
}
final randomSeconds = Random().nextInt(2 * 24 * 60 * 60);
return now.subtract(Duration(seconds: randomSeconds));
}

/// Creates a NIP-59 encrypted event with the following structure:
/// 1. Inner event (kind 1): Original content
/// 2. Seal event (kind 13): Encrypted inner event
/// 3. Wrapper event (kind 1059): Final encrypted package
static Future<NostrEvent> createNIP59Event(
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Support for encrypting DMs to send to the mostro daemon as per issue #3

String content, String recipientPubKey, String senderPrivateKey) async {
// Validate inputs
if (content.isEmpty) throw ArgumentError('Content cannot be empty');
if (recipientPubKey.length != 64) {
throw ArgumentError('Invalid recipient public key');
}
if (!isValidPrivateKey(senderPrivateKey)) {
throw ArgumentError('Invalid sender private key');
}

final encryptedContent = _encryptNIP44(content, sharedSecret);
final senderKeyPair = generateKeyPairFromPrivateKey(senderPrivateKey);

final createdAt = DateTime.now();
final rumorEvent = NostrEvent(
kind: 1059,
pubkey: senderKeyPair.public,
content: encryptedContent,
final rumorEvent = NostrEvent.fromPartialData(
kind: 1,
keyPairs: senderKeyPair,
content: content,
createdAt: createdAt,
tags: [
["p", recipientPubKey]
],
createdAt: createdAt,
id: '', // Se generará después
sig: '', // Se generará después
);

// Generar ID y firma
final id = generateId({
'pubkey': rumorEvent.pubkey,
'created_at': rumorEvent.createdAt!.millisecondsSinceEpoch ~/ 1000,
'kind': rumorEvent.kind,
'tags': rumorEvent.tags,
'content': rumorEvent.content,
});
signMessage(id, senderPrivateKey);
String? encryptedContent;

try {
encryptedContent = await _encryptNIP44(
jsonEncode(rumorEvent.toMap()), senderPrivateKey, recipientPubKey);
} catch (e) {
throw Exception('Failed to encrypt content: $e');
}

final sealEvent = NostrEvent.fromPartialData(
kind: 13,
keyPairs: senderKeyPair,
content: encryptedContent,
createdAt: randomNow(),
);

final wrapperKeyPair = generateKeyPair();
final wrappedContent = _encryptNIP44(jsonEncode(rumorEvent.toMap()),
_calculateSharedSecret(wrapperKeyPair.private, recipientPubKey));

return NostrEvent(
final pk = wrapperKeyPair.private;

String sealedContent;
try {
sealedContent = await _encryptNIP44(
jsonEncode(sealEvent.toMap()), pk, '02$recipientPubKey');
} catch (e) {
throw Exception('Failed to encrypt seal event: $e');
}

final wrapEvent = NostrEvent.fromPartialData(
kind: 1059,
pubkey: wrapperKeyPair.public,
content: wrappedContent,
content: sealedContent,
keyPairs: wrapperKeyPair,
tags: [
["p", recipientPubKey]
],
createdAt: DateTime.now(),
id: generateId({
'pubkey': wrapperKeyPair.public,
'created_at': DateTime.now().millisecondsSinceEpoch ~/ 1000,
'kind': 1059,
'tags': [
["p", recipientPubKey]
],
'content': wrappedContent,
}),
sig: '', // Se generará automáticamente al publicar el evento
createdAt: createdAt,
);
}

static String decryptNIP59Event(NostrEvent event, String privateKey) {
final sharedSecret = _calculateSharedSecret(privateKey, event.pubkey);
final decryptedContent = _decryptNIP44(event.content ?? '', sharedSecret);
return wrapEvent;
}

final rumorEvent = NostrEvent.deserialized(decryptedContent);
final rumorSharedSecret =
_calculateSharedSecret(privateKey, rumorEvent.pubkey);
final finalDecryptedContent =
_decryptNIP44(rumorEvent.content ?? '', rumorSharedSecret);
static Future<NostrEvent> decryptNIP59Event(
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Support for decrypting DMs from mostro as per issue #3

NostrEvent event, String privateKey) async {
// Validate inputs
if (event.content == null || event.content!.isEmpty) {
throw ArgumentError('Event content is empty');
}
if (!isValidPrivateKey(privateKey)) {
throw ArgumentError('Invalid private key');
}

return finalDecryptedContent;
try {
final decryptedContent =
await _decryptNIP44(event.content ?? '', privateKey, event.pubkey);

final rumorEvent =
NostrEvent.deserialized('["EVENT", "", $decryptedContent]');

final finalDecryptedContent = await _decryptNIP44(
rumorEvent.content ?? '', privateKey, rumorEvent.pubkey);

final wrap = jsonDecode(finalDecryptedContent) as Map<String, dynamic>;

// Validate decrypted event structure
_validateEventStructure(wrap);

return NostrEvent(
id: wrap['id'] as String,
kind: wrap['kind'] as int,
content: wrap['content'] as String,
sig: "",
pubkey: wrap['pubkey'] as String,
createdAt: DateTime.fromMillisecondsSinceEpoch(
(wrap['created_at'] as int) * 1000,
),
tags: List<List<String>>.from(
(wrap['tags'] as List)
.map(
(nestedElem) => (nestedElem as List)
.map(
(nestedElemContent) => nestedElemContent.toString(),
)
.toList(),
)
.toList(),
),
subscriptionId: '',
);
} catch (e) {
throw Exception('Failed to decrypt NIP-59 event: $e');
}
Comment on lines +216 to +265
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add additional validation for decrypted content.

The current implementation could benefit from additional validation steps:

  1. Content format validation
  2. Event type validation
  3. Timestamp validation
   static Future<NostrEvent> decryptNIP59Event(
       NostrEvent event, String privateKey) async {
     // ... existing validation ...

     try {
       final decryptedContent =
           await _decryptNIP44(event.content ?? '', privateKey, event.pubkey);

+      // Validate JSON format before parsing
+      if (!_isValidJson(decryptedContent)) {
+        throw FormatException('Decrypted content is not valid JSON');
+      }

       final rumorEvent =
           NostrEvent.deserialized('["EVENT", "", $decryptedContent]');

+      // Validate event type
+      if (rumorEvent.kind != 1) {
+        throw FormatException('Invalid rumor event type: ${rumorEvent.kind}');
+      }

       // ... rest of the implementation ...
     }
   }

+  static bool _isValidJson(String str) {
+    try {
+      json.decode(str);
+      return true;
+    } catch (e) {
+      return false;
+    }
+  }

Committable suggestion skipped: line range outside the PR's diff.

}

static Uint8List _calculateSharedSecret(String privateKey, String publicKey) {
// Nota: Esta implementación puede necesitar ajustes dependiendo de cómo
// dart_nostr maneje la generación de secretos compartidos.
// Posiblemente necesites usar una biblioteca de criptografía adicional aquí.
final sharedPoint = generateKeyPairFromPrivateKey(privateKey).public;
return Uint8List.fromList(sha256.convert(utf8.encode(sharedPoint)).bytes);
/// Validates the structure of a decrypted event
static void _validateEventStructure(Map<String, dynamic> event) {
final requiredFields = [
'id',
'kind',
'content',
'pubkey',
'created_at',
'tags'
];
for (final field in requiredFields) {
if (!event.containsKey(field)) {
throw FormatException('Missing required field: $field');
}
}
Comment on lines +268 to +282
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add type validation for event fields.

The current implementation only checks for field presence but not their types.

   static void _validateEventStructure(Map<String, dynamic> event) {
-    final requiredFields = [
-      'id',
-      'kind',
-      'content',
-      'pubkey',
-      'created_at',
-      'tags'
-    ];
+    final requiredFields = {
+      'id': String,
+      'kind': int,
+      'content': String,
+      'pubkey': String,
+      'created_at': int,
+      'tags': List
+    };
-    for (final field in requiredFields) {
+    for (final field in requiredFields.entries) {
       if (!event.containsKey(field)) {
-        throw FormatException('Missing required field: $field');
+        throw FormatException('Missing required field: ${field.key}');
+      }
+      if (event[field.key].runtimeType != field.value) {
+        throw FormatException(
+            'Invalid type for ${field.key}: expected ${field.value}, got ${event[field.key].runtimeType}');
       }
     }
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/// Validates the structure of a decrypted event
static void _validateEventStructure(Map<String, dynamic> event) {
final requiredFields = [
'id',
'kind',
'content',
'pubkey',
'created_at',
'tags'
];
for (final field in requiredFields) {
if (!event.containsKey(field)) {
throw FormatException('Missing required field: $field');
}
}
/// Validates the structure of a decrypted event
static void _validateEventStructure(Map<String, dynamic> event) {
final requiredFields = {
'id': String,
'kind': int,
'content': String,
'pubkey': String,
'created_at': int,
'tags': List
};
for (final field in requiredFields.entries) {
if (!event.containsKey(field)) {
throw FormatException('Missing required field: ${field.key}');
}
if (event[field.key].runtimeType != field.value) {
throw FormatException(
'Invalid type for ${field.key}: expected ${field.value}, got ${event[field.key].runtimeType}');
}
}

}

static String _encryptNIP44(String content, Uint8List key) {
final iv = encrypt.IV.fromSecureRandom(16);
final encrypter = encrypt.Encrypter(encrypt.AES(encrypt.Key(key)));
final encrypted = encrypter.encrypt(content, iv: iv);
return base64Encode(iv.bytes + encrypted.bytes);
static Future<String> _encryptNIP44(
String content, String privkey, String pubkey) async {
try {
return await Nip44.encryptMessage(content, privkey, pubkey);
} catch (e) {
// Handle encryption error appropriately
throw Exception('Encryption failed: $e');
}
}

static String _decryptNIP44(String encryptedContent, Uint8List key) {
final decoded = base64Decode(encryptedContent);
final iv = encrypt.IV(decoded.sublist(0, 16));
final encryptedBytes = decoded.sublist(16);
final encrypter = encrypt.Encrypter(encrypt.AES(encrypt.Key(key)));
return encrypter.decrypt64(base64Encode(encryptedBytes), iv: iv);
static Future<String> _decryptNIP44(
String encryptedContent, String privkey, String pubkey) async {
try {
return await Nip44.decryptMessage(encryptedContent, privkey, pubkey);
} catch (e) {
// Handle encryption error appropriately
throw Exception('Decryption failed: $e');
}
Comment on lines +285 to +302
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add input validation to NIP-44 encryption/decryption methods.

Both methods should validate their inputs before proceeding with the operation.

  static Future<String> _encryptNIP44(
      String content, String privkey, String pubkey) async {
+   if (content.isEmpty) throw ArgumentError('Content cannot be empty');
+   if (privkey.length != 64) throw ArgumentError('Invalid private key length');
+   if (!pubkey.startsWith('02')) throw ArgumentError('Invalid public key format');
    try {
      return await Nip44.encryptMessage(content, privkey, pubkey);
    } catch (e) {
      throw Exception('Encryption failed: $e');
    }
  }

  static Future<String> _decryptNIP44(
      String encryptedContent, String privkey, String pubkey) async {
+   if (encryptedContent.isEmpty) throw ArgumentError('Encrypted content cannot be empty');
+   if (privkey.length != 64) throw ArgumentError('Invalid private key length');
+   if (!pubkey.startsWith('02')) throw ArgumentError('Invalid public key format');
    try {
      return await Nip44.decryptMessage(encryptedContent, privkey, pubkey);
    } catch (e) {
      throw Exception('Decryption failed: $e');
    }
  }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
static Future<String> _encryptNIP44(
String content, String privkey, String pubkey) async {
try {
return await Nip44.encryptMessage(content, privkey, pubkey);
} catch (e) {
// Handle encryption error appropriately
throw Exception('Encryption failed: $e');
}
}
static String _decryptNIP44(String encryptedContent, Uint8List key) {
final decoded = base64Decode(encryptedContent);
final iv = encrypt.IV(decoded.sublist(0, 16));
final encryptedBytes = decoded.sublist(16);
final encrypter = encrypt.Encrypter(encrypt.AES(encrypt.Key(key)));
return encrypter.decrypt64(base64Encode(encryptedBytes), iv: iv);
static Future<String> _decryptNIP44(
String encryptedContent, String privkey, String pubkey) async {
try {
return await Nip44.decryptMessage(encryptedContent, privkey, pubkey);
} catch (e) {
// Handle encryption error appropriately
throw Exception('Decryption failed: $e');
}
static Future<String> _encryptNIP44(
String content, String privkey, String pubkey) async {
if (content.isEmpty) throw ArgumentError('Content cannot be empty');
if (privkey.length != 64) throw ArgumentError('Invalid private key length');
if (!pubkey.startsWith('02')) throw ArgumentError('Invalid public key format');
try {
return await Nip44.encryptMessage(content, privkey, pubkey);
} catch (e) {
// Handle encryption error appropriately
throw Exception('Encryption failed: $e');
}
}
static Future<String> _decryptNIP44(
String encryptedContent, String privkey, String pubkey) async {
if (encryptedContent.isEmpty) throw ArgumentError('Encrypted content cannot be empty');
if (privkey.length != 64) throw ArgumentError('Invalid private key length');
if (!pubkey.startsWith('02')) throw ArgumentError('Invalid public key format');
try {
return await Nip44.decryptMessage(encryptedContent, privkey, pubkey);
} catch (e) {
// Handle encryption error appropriately
throw Exception('Decryption failed: $e');
}

}
}
Loading