Skip to content

Commit

Permalink
refactor: PR #25
Browse files Browse the repository at this point in the history
  • Loading branch information
ethicnology committed Apr 17, 2023
1 parent 37f5645 commit 385f211
Show file tree
Hide file tree
Showing 6 changed files with 132 additions and 229 deletions.
64 changes: 5 additions & 59 deletions lib/src/event.dart
Original file line number Diff line number Diff line change
@@ -1,12 +1,9 @@
import 'dart:convert';
import 'dart:typed_data';
import 'package:bip340/bip340.dart' as bip340;
import 'package:convert/convert.dart';
import 'package:pointycastle/export.dart';

import 'nips/nip_004/crypto.dart';
import 'utils.dart';
import 'settings.dart';
import 'package:bip340/bip340.dart' as bip340;
import 'package:nostr/src/utils.dart';

/// The only object type that exists is the event, which has the following format on the wire:
///
Expand Down Expand Up @@ -49,11 +46,6 @@ class Event {
/// subscription_id is a random string that should be used to represent a subscription.
String? subscriptionId;

/// Nip04: If event is of kind 4, then `decrypted` flag indicates whether `content` was
/// successfully decrypted. Unsuccessful decryption on a valid event is typically caused
/// by missing or mismatched private key.
bool decrypted = false;

/// Default constructor
///
/// verify: ensure your event isValid() –> id, signature, timestamp…
Expand Down Expand Up @@ -262,14 +254,11 @@ class Event {
throw Exception('invalid input');
}

if (json['tags'] is String) {

This comment has been minimized.

Copy link
@ntheden

ntheden Apr 17, 2023

Contributor

I had this change in there because a python relay I was talking to escaped the " character in nested json objects so jsonDecode left it as a string in first pass.

This comment has been minimized.

Copy link
@ethicnology

ethicnology Apr 18, 2023

Author Owner

Was this only with this python relay ?

This comment has been minimized.

Copy link
@ntheden

ntheden Apr 18, 2023

Contributor

I've only seen it from one relay, but I can't verify which relay it was right now. I will take note next time.

This comment has been minimized.

Copy link
@ntheden

ntheden Apr 18, 2023

Contributor

Ok it happens with monstr_relay:
https://github.com/monty888/monstr_terminal.git

It happens in Event.deserialize():

type 'String' is not a subtype of type 'List<dynamic>' in type cast
#0      new Event.deserialize (package:nostr/src/event.dart:257:30)
#1      new Message.deserialize (package:nostr/src/message.dart:25:25)                                             
#2      receiveStuff.<anonymous closure> (file:///sdpool/home/builder/src/dart-nostr/n.dart:194:25)
#3      _RootZone.runUnaryGuarded (dart:async/zone.dart:1593:10)                                  
#4      _BufferingStreamSubscription._sendData (dart:async/stream_impl.dart:339:11)                
#5      _BufferingStreamSubscription._add (dart:async/stream_impl.dart:271:7)
#6      _SyncStreamControllerDispatch._sendData (dart:async/stream_controller.dart:774:19)                              
#7      _StreamController._add (dart:async/stream_controller.dart:648:7)                                                
#8      _StreamController.add (dart:async/stream_controller.dart:596:5)                                                 
#9      new _WebSocketImpl._fromSocket.<anonymous closure> (dart:_http/websocket_impl.dart:1144:21)                     
#10     _RootZone.runUnaryGuarded (dart:async/zone.dart:1593:10)                                                        
#11     _BufferingStreamSubscription._sendData (dart:async/stream_impl.dart:339:11)                                     
#12     _BufferingStreamSubscription._add (dart:async/stream_impl.dart:271:7)                                           
#13     _SinkTransformerStreamSubscription._add (dart:async/stream_transformers.dart:63:11)                             
#14     _EventSinkWrapper.add (dart:async/stream_transformers.dart:13:11)                                               
#15     _WebSocketProtocolTransformer._messageFrameEnd (dart:_http/websocket_impl.dart:332:23)                          
#16     _WebSocketProtocolTransformer.add (dart:_http/websocket_impl.dart:226:46)                                       
#17     _SinkTransformerStreamSubscription._handleData (dart:async/stream_transformers.dart:111:24)                     
#18     _RootZone.runUnaryGuarded (dart:async/zone.dart:1593:10)
#19     _BufferingStreamSubscription._sendData (dart:async/stream_impl.dart:339:11)
#20     _BufferingStreamSubscription._add (dart:async/stream_impl.dart:271:7)                                           
#21     _SyncStreamControllerDispatch._sendData (dart:async/stream_controller.dart:774:19)                              
#22     _StreamController._add (dart:async/stream_controller.dart:648:7)
#23     _StreamController.add (dart:async/stream_controller.dart:596:5)                    
#24     _Socket._onData (dart:io-patch/socket_patch.dart:2355:41)                                                       
#25     _RootZone.runUnaryGuarded (dart:async/zone.dart:1593:10)                                                        
#26     _BufferingStreamSubscription._sendData (dart:async/stream_impl.dart:339:11)
#27     _BufferingStreamSubscription._add (dart:async/stream_impl.dart:271:7)                                           
#28     _SyncStreamControllerDispatch._sendData (dart:async/stream_controller.dart:774:19)                              
#29     _StreamController._add (dart:async/stream_controller.dart:648:7)                                                
#30     _StreamController.add (dart:async/stream_controller.dart:596:5)                                                 
#31     new _RawSocket.<anonymous closure> (dart:io-patch/socket_patch.dart:1877:33)                                    
#32     _NativeSocket.issueReadEvent.issue (dart:io-patch/socket_patch.dart:1334:14)                                    
#33     _microtaskLoop (dart:async/schedule_microtask.dart:40:21)                                                       
#34     _startMicrotaskLoop (dart:async/schedule_microtask.dart:49:5)                                                   
#35     _runPendingImmediateCallback (dart:isolate-patch/isolate_patch.dart:123:13)              
#36     _Timer._runTimers (dart:isolate-patch/timer_impl.dart:405:11)                               
#37     _Timer._handleMessage (dart:isolate-patch/timer_impl.dart:429:5)                                                
#38     _RawReceivePort._handleMessage (dart:isolate-patch/isolate_patch.dart:192:26)

Probably best not to work around that bug and instead I will submit a PR to monstr to fix it, unless someone else does first.

This comment has been minimized.

Copy link
@ethicnology

ethicnology Apr 19, 2023

Author Owner

https://ethicnology.github.io/dispute/#/ works with nostr rust relay

You can play with the same endpoint if you want https://relay.dispute.systems/

json['tags'] = jsonDecode(json['tags']);
}
List<List<String>> tags = (json['tags'] as List<dynamic>)
var tags = (json['tags'] as List<dynamic>)
.map((e) => (e as List<dynamic>).map((e) => e as String).toList())
.toList();

Event event = Event(
return Event(
json['id'],
json['pubkey'],
json['created_at'],
Expand All @@ -280,24 +269,6 @@ class Event {
subscriptionId: subscriptionId,
verify: verify,
);
if (event.kind == 4) {
event.nip04Decrypt();
}
return event;
}

factory Event.newEvent(
String content,
String privkey,
) {
Event event = Event.partial();
event.kind = 1;
event.content = content;
event.createdAt = currentUnixTimestampSeconds();
event.pubkey = bip340.getPublicKey(privkey).toLowerCase();
event.id = event.getEventId();
event.sig = event.getSignature(privkey);
return event;
}

/// To obtain the event.id, we sha256 the serialized event.
Expand Down Expand Up @@ -331,7 +302,7 @@ class Event {
String content,
) {
List data = [0, pubkey.toLowerCase(), createdAt, kind, tags, content];
String serializedEvent = jsonEncode(data);
String serializedEvent = json.encode(data);
Uint8List hash = SHA256Digest()
.process(Uint8List.fromList(utf8.encode(serializedEvent)));
return hex.encode(hash);
Expand All @@ -357,14 +328,6 @@ class Event {
/// Verify if event checks such as id, signature, non-futuristic are valid
/// Performances could be a reason to disable event checks
bool isValid() {
if (decrypted) {
// isValid() check was already performed when the Event was created at
// deserialization off of the input stream. Post-decryption, id check will
// fail as the content has changed.
// Alternatively, getEventId() could compute id from a `plaintext` field
// when `decrypted` is true.
return true;
}
String verifyId = getEventId();
if (createdAt.toString().length == 10 &&
id == verifyId &&
Expand All @@ -374,21 +337,4 @@ class Event {
return false;
}
}

bool nip04Decrypt() {
int ivIndex = content.indexOf("?iv=");
if (ivIndex <= 0) {
print("Invalid content for dm, could not get ivIndex: $content");
return false;
}
String iv = content.substring(ivIndex + "?iv=".length, content.length);
String encString = content.substring(0, ivIndex);
try {
content = Nip04.decrypt(userPrivateKey, "02$pubkey", encString, iv);
decrypted = true;
} catch (e) {
//print("Fail to decrypt: ${e}");
}
return decrypted;
}
}
1 change: 1 addition & 0 deletions lib/src/message.dart
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ class Message {
switch (type) {
case "EVENT":
message = Event.deserialize(data);
if (message.kind == 4) message = EncryptedDirectMessage(message);

This comment has been minimized.

Copy link
@ethicnology

ethicnology Apr 18, 2023

Author Owner

@no-prob

I suppose the usage model for receive side is if kind=4 then cast the Event to EncryptedDirectMessage and call getPlaintext?

Yes

This comment has been minimized.

Copy link
@ethicnology

ethicnology Apr 18, 2023

Author Owner
break;
case "REQ":
message = Request.deserialize(data);
Expand Down
64 changes: 29 additions & 35 deletions lib/src/nips/nip_004/crypto.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,23 @@ import 'package:pointycastle/export.dart';

import '../../crypto/kepler.dart';

class Nip04 {
class Nip4 {
static Map<String, List<List<int>>> gMapByteSecret = {};
// pointy castle source https://github.com/PointyCastle/pointycastle/blob/master/tutorials/aes-cbc.md
// https://github.com/bcgit/pc-dart/blob/master/tutorials/aes-cbc.md
// 3 https://github.com/Dhuliang/flutter-bsv/blob/42a2d92ec6bb9ee3231878ffe684e1b7940c7d49/lib/src/aescbc.dart

// Encrypt data using self private key in nostr format ( with trailing ?iv=)
static String encryptMessage(
String privateString, String publicString, String plainText) {
Uint8List uintInputText = Utf8Encoder().convert(plainText);
final encryptedString =
encryptMessageRaw(privateString, publicString, uintInputText);
return encryptedString;
}

static String encryptMessageRaw(
String privateString, String publicString, Uint8List uintInputText) {
final secretIV = Kepler.byteSecret(privateString, publicString);
final key = Uint8List.fromList(secretIV[0]);
static String cipher(
String privkey,
String pubkey,
String plaintext,
) {
Uint8List uintInputText = Utf8Encoder().convert(plaintext);
final secretIV = Kepler.byteSecret(privkey, pubkey);
final key = Uint8List.fromList(
secretIV[0],
);

// generate iv https://stackoverflow.com/questions/63630661/aes-engine-not-initialised-with-pointycastle-securerandom
FortunaRandom fr = FortunaRandom();
Expand Down Expand Up @@ -60,31 +61,23 @@ class Nip04 {
return outputPlainText;
}

// pointy castle source https://github.com/PointyCastle/pointycastle/blob/master/tutorials/aes-cbc.md
// https://github.com/bcgit/pc-dart/blob/master/tutorials/aes-cbc.md
// 3 https://github.com/Dhuliang/flutter-bsv/blob/42a2d92ec6bb9ee3231878ffe684e1b7940c7d49/lib/src/aescbc.dart

/// Decrypt data using self private key
static String decrypt(
String privateString, String publicString, String b64encoded,
[String b64IV = ""]) {
Uint8List encdData = base64.decode(b64encoded);
final rawData = decryptRaw(privateString, publicString, encdData, b64IV);
return Utf8Decoder().convert(rawData.toList());
}

static Uint8List decryptRaw(
String privateString, String publicString, Uint8List cipherText,
[String b64IV = ""]) {
List<List<int>> byteSecret = gMapByteSecret[publicString] ?? [];
// Decrypt data using self private key
static String decipher(
String privkey,
String pubkey,
String ciphertext, [
String nonce = "",
]) {
Uint8List cipherText = base64.decode(ciphertext);
List<List<int>> byteSecret = gMapByteSecret[pubkey] ?? [];
if (byteSecret.isEmpty) {
byteSecret = Kepler.byteSecret(privateString, publicString);
gMapByteSecret[publicString] = byteSecret;
byteSecret = Kepler.byteSecret(privkey, pubkey);
gMapByteSecret[pubkey] = byteSecret;
}
final secretIV = byteSecret;
final key = Uint8List.fromList(secretIV[0]);
final iv = b64IV.length > 6
? base64.decode(b64IV)
final iv = nonce.length > 6
? base64.decode(nonce)
: Uint8List.fromList(secretIV[1]);

CipherParameters params = PaddedBlockCipherParameters(
Expand All @@ -107,6 +100,7 @@ class Nip04 {
}
//remove padding
offset += cipherImpl.doFinal(cipherText, offset, finalPlainText, offset);
return finalPlainText.sublist(0, offset);
Uint8List result = finalPlainText.sublist(0, offset);
return Utf8Decoder().convert(result.toList());
}
}
130 changes: 48 additions & 82 deletions lib/src/nips/nip_004/event.dart
Original file line number Diff line number Diff line change
@@ -1,93 +1,59 @@
import 'package:bip340/bip340.dart' as bip340;

import '../../event.dart';
import '../../utils.dart';
import 'crypto.dart';
import 'package:nostr/src/event.dart';
import 'package:nostr/src/nips/nip_004/crypto.dart';
import 'package:nostr/src/utils.dart';

class EncryptedDirectMessage extends Event {
late String peerPubkey;
late String? plaintext;
late String? referenceEventId;

EncryptedDirectMessage(
this.peerPubkey,
id,
pubkey,
createdAt,
kind,
tags,
content,
sig, {
subscriptionId,
bool verify = false,
this.plaintext,
this.referenceEventId,
}) : super(
id,
pubkey,
createdAt,
kind,
tags,
content,
sig,
subscriptionId: subscriptionId,
verify: verify,
) {
kind = 4;
plaintext = content;
}
static Map<String, List<List<int>>> gMapByteSecret = {};

factory EncryptedDirectMessage.partial({
peerPubkey = "",
id = "",
pubkey = "",
createdAt = 0,
kind = 4,
tags = const <List<String>>[],
content = "",
sig = "",
plaintext,
referenceEventId,
subscriptionId,
bool verify = false,
}) {
return EncryptedDirectMessage(
peerPubkey,
id,
pubkey,
createdAt,
kind,
tags,
content,
sig,
plaintext: plaintext,
referenceEventId: referenceEventId,
subscriptionId: subscriptionId,
verify: verify,
);
}
EncryptedDirectMessage(Event event)
: super(
event.id,
event.pubkey,
event.createdAt,
4,
event.tags,
event.content,
event.sig,
subscriptionId: event.subscriptionId,
verify: true,
);

factory EncryptedDirectMessage.newEvent(
String peerPubkey,
String plaintext,
String privkey, {
String? referenceEventId,
}) {
EncryptedDirectMessage event = EncryptedDirectMessage.partial();
event.content = Nip04.encryptMessage(privkey, '02$peerPubkey', plaintext);
event.kind = 4;
factory EncryptedDirectMessage.quick(
String senderPrivkey,
String receiverPubkey,
String message,
) {
var event = Event.partial();
event.pubkey = bip340.getPublicKey(senderPrivkey).toLowerCase();
event.createdAt = currentUnixTimestampSeconds();
event.pubkey = bip340.getPublicKey(privkey).toLowerCase();
event.kind = 4;
event.tags = [
['p', peerPubkey],
['p', receiverPubkey]
];
event.peerPubkey = peerPubkey;
event.plaintext = plaintext;
if (referenceEventId != null) {
event.tags.add(['e', referenceEventId]);
}
event.content = Nip4.cipher(senderPrivkey, '02$receiverPubkey', message);
event.id = event.getEventId();
event.sig = event.getSignature(privkey);
return event;
event.sig = event.getSignature(senderPrivkey);
return EncryptedDirectMessage(event);
}

String? get receiverPubkey => findPubkey();

String getCiphertext(String senderPrivkey, String receiverPubkey) {
String ciphertext =
Nip4.cipher(senderPrivkey, '02$receiverPubkey', content);
return ciphertext;
}

String getPlaintext(String receiverPrivkey, String senderPubkey) {

This comment has been minimized.

Copy link
@ntheden

ntheden Apr 19, 2023

Contributor

I don't think senderPubkey needs to be a parameter since it is already the pubkey of the received event. Can just do

    return Nip4.decipher(receiverPrivkey, pubkey, content);
return Nip4.decipher(receiverPrivkey, senderPubkey, content);
}

String? findPubkey() {
String prefix = "p";
for (List<String> tag in tags) {
if (tag.isNotEmpty && tag[0] == prefix && tag.length > 1) return tag[1];
}
return null;
}
}
2 changes: 0 additions & 2 deletions lib/src/settings.dart

This file was deleted.

Loading

2 comments on commit 385f211

@ntheden
Copy link
Contributor

Choose a reason for hiding this comment

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

There seems to be some bug, I can't get the code in this commit to decrypt events without errors.

@ntheden
Copy link
Contributor

Choose a reason for hiding this comment

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

There seems to be some bug, I can't get the code in this commit to decrypt events without errors.

I fixed the receive issue in PR #26 . I created it to show the change, still need to add in the unit tests though.

Please sign in to comment.