diff --git a/lib/src/event.dart b/lib/src/event.dart index 2748f4b..96f7c25 100644 --- a/lib/src/event.dart +++ b/lib/src/event.dart @@ -128,27 +128,26 @@ class Event { ); } - /// Instantiate Event object from the minimum available data + /// Instantiate Event object from the minimum needed data /// /// ```dart ///Event event = Event.from( /// kind: 1, - /// tags: [], /// content: "", /// privkey: /// "5ee1c8000ab28edd64d74a7d951ac2dd559814887b1b9e1ac7c5f89e96125c12", ///); ///``` factory Event.from({ - int createdAt = 0, + int? createdAt, required int kind, - required List> tags, + List> tags = const [], required String content, required String privkey, String? subscriptionId, bool verify = false, }) { - if (createdAt == 0) createdAt = currentUnixTimestampSeconds(); + createdAt ??= currentUnixTimestampSeconds(); final pubkey = bip340.getPublicKey(privkey).toLowerCase(); final id = _processEventId( @@ -271,20 +270,6 @@ class Event { ); } - factory Event.quick( - 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. /// The serialization is done over the UTF-8 JSON-serialized string (with no white space or line breaks) of the following structure: /// diff --git a/lib/src/nips/nip_004/crypto.dart b/lib/src/nips/nip_004/crypto.dart index f342b87..3365b52 100644 --- a/lib/src/nips/nip_004/crypto.dart +++ b/lib/src/nips/nip_004/crypto.dart @@ -1,105 +1,66 @@ import 'dart:convert'; import 'dart:math'; import 'dart:typed_data'; +import 'package:nostr/src/crypto/kepler.dart'; import 'package:pointycastle/export.dart'; -import '../../crypto/kepler.dart'; - class Nip4 { - static Map>> 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 cipher( - String privkey, - String pubkey, - String plaintext, - ) { - Uint8List uintInputText = Utf8Encoder().convert(plaintext); - final secretIV = Kepler.byteSecret(privkey, pubkey); - final key = Uint8List.fromList( - secretIV[0], + static Uint8List generateNonce() { + final random = Random.secure(); + return Uint8List.fromList( + List.generate(16, (i) => random.nextInt(256)), ); - - // generate iv https://stackoverflow.com/questions/63630661/aes-engine-not-initialised-with-pointycastle-securerandom - FortunaRandom fr = FortunaRandom(); - final sGen = Random.secure(); - fr.seed(KeyParameter( - Uint8List.fromList(List.generate(32, (_) => sGen.nextInt(255))))); - final iv = fr.nextBytes(16); - - CipherParameters params = PaddedBlockCipherParameters( - ParametersWithIV(KeyParameter(key), iv), null); - - PaddedBlockCipherImpl cipherImpl = - PaddedBlockCipherImpl(PKCS7Padding(), CBCBlockCipher(AESEngine())); - - cipherImpl.init( - true, // means to encrypt - params as PaddedBlockCipherParameters); - - // allocate space - final Uint8List outputEncodedText = Uint8List(uintInputText.length + 16); - - var offset = 0; - while (offset < uintInputText.length - 16) { - offset += cipherImpl.processBlock( - uintInputText, offset, outputEncodedText, offset); - } - - //add padding - offset += - cipherImpl.doFinal(uintInputText, offset, outputEncodedText, offset); - final Uint8List finalEncodedText = outputEncodedText.sublist(0, offset); - - String stringIv = base64.encode(iv); - String outputPlainText = base64.encode(finalEncodedText); - outputPlainText = "$outputPlainText?iv=$stringIv"; - return outputPlainText; } - static String decipher( + static String cipher( String privkey, String pubkey, - String ciphertext, [ - String nonce = "", - ]) { - Uint8List cipherText = base64.decode(ciphertext); - List> byteSecret = gMapByteSecret[pubkey] ?? []; - if (byteSecret.isEmpty) { - byteSecret = Kepler.byteSecret(privkey, pubkey); - gMapByteSecret[pubkey] = byteSecret; + String payload, + bool cipher, { + String? nonce, + }) { + // if cipher=false –> decipher –> nonce needed + if (!cipher && nonce == null) throw Exception("missing nonce"); + + // init variables + Uint8List input, output, iv; + if (!cipher && nonce != null) { + input = base64.decode(payload); + output = Uint8List(input.length); + iv = base64.decode(nonce); + } else { + input = Utf8Encoder().convert(payload); + output = Uint8List(input.length + 16); + iv = generateNonce(); } - final secretIV = byteSecret; - final key = Uint8List.fromList(secretIV[0]); - final iv = nonce.length > 6 - ? base64.decode(nonce) - : Uint8List.fromList(secretIV[1]); - CipherParameters params = PaddedBlockCipherParameters( - ParametersWithIV(KeyParameter(key), iv), null); - - PaddedBlockCipherImpl cipherImpl = - PaddedBlockCipherImpl(PKCS7Padding(), CBCBlockCipher(AESEngine())); - - cipherImpl.init( - false, - params as PaddedBlockCipherParameters); - final Uint8List finalPlainText = - Uint8List(cipherText.length); // allocate space + // params + List> keplerSecret = Kepler.byteSecret(privkey, pubkey); + var key = Uint8List.fromList(keplerSecret[0]); + var params = PaddedBlockCipherParameters( + ParametersWithIV(KeyParameter(key), iv), + null, + ); + var algo = PaddedBlockCipherImpl( + PKCS7Padding(), + CBCBlockCipher(AESEngine()), + ); + // processing + algo.init(cipher, params); var offset = 0; - while (offset < cipherText.length - 16) { - offset += - cipherImpl.processBlock(cipherText, offset, finalPlainText, offset); + while (offset < input.length - 16) { + offset += algo.processBlock(input, offset, output, offset); + } + offset += algo.doFinal(input, offset, output, offset); + Uint8List result = output.sublist(0, offset); + + if (cipher) { + String stringIv = base64.encode(iv); + String plaintext = base64.encode(result); + return "$plaintext?iv=$stringIv"; + } else { + return Utf8Decoder().convert(result); } - //remove padding - offset += cipherImpl.doFinal(cipherText, offset, finalPlainText, offset); - Uint8List result = finalPlainText.sublist(0, offset); - return Utf8Decoder().convert(result); } } diff --git a/lib/src/nips/nip_004/event.dart b/lib/src/nips/nip_004/event.dart index f437f03..6cbb0d6 100644 --- a/lib/src/nips/nip_004/event.dart +++ b/lib/src/nips/nip_004/event.dart @@ -4,8 +4,6 @@ import 'package:nostr/src/nips/nip_004/crypto.dart'; import 'package:nostr/src/utils.dart'; class EncryptedDirectMessage extends Event { - static Map>> gMapByteSecret = {}; - EncryptedDirectMessage(Event event) : super( event.id, @@ -31,7 +29,12 @@ class EncryptedDirectMessage extends Event { event.tags = [ ['p', receiverPubkey] ]; - event.content = Nip4.cipher(senderPrivkey, '02$receiverPubkey', message); + event.content = Nip4.cipher( + senderPrivkey, + '02$receiverPubkey', + message, + true, + ); event.id = event.getEventId(); event.sig = event.getSignature(senderPrivkey); return EncryptedDirectMessage(event); @@ -39,28 +42,29 @@ class EncryptedDirectMessage extends Event { String? get receiverPubkey => findPubkey(); + String get nonce { + List split = content.split("?iv="); + if (split.length != 2) throw Exception("invalid nip4 content"); + return split[1]; + } + String getCiphertext(String senderPrivkey, String receiverPubkey) { - String ciphertext = - Nip4.cipher(senderPrivkey, '02$receiverPubkey', content); - return ciphertext; + return Nip4.cipher(senderPrivkey, '02$receiverPubkey', content, true); } - String getPlaintext(String receiverPrivkey, [String senderPubkey=""]) { - if (senderPubkey.length == 0) { - senderPubkey = pubkey; - } + String getPlaintext(String receiverPrivkey) { + String ciphertext = content.split("?iv=")[0]; String plaintext = ""; - int ivIndex = content.indexOf("?iv="); - if( ivIndex <= 0) { - print("Invalid content for dm, could not get ivIndex: $content"); - return plaintext; - } - String iv = content.substring(ivIndex + "?iv=".length, content.length); - String ciphertext = content.substring(0, ivIndex); try { - plaintext = Nip4.decipher(receiverPrivkey, "02$senderPubkey", ciphertext, iv); - } catch(e) { - print("Fail to decrypt: ${e}"); + plaintext = Nip4.cipher( + receiverPrivkey, + "02$pubkey", + ciphertext, + false, + nonce: nonce, + ); + } catch (e) { + throw Exception("Fail to decipher: $e"); } return plaintext; }