From 7922879ed7d572761c6a2f17fb7e311922776434 Mon Sep 17 00:00:00 2001 From: no-prob <113266379+no-prob@users.noreply.github.com> Date: Sun, 16 Apr 2023 09:57:55 -0700 Subject: [PATCH 01/16] feat: Add NIP04 support (#25) * add encrypted direct messages (NIP04) support, with code from https://github.com/vishalxl/nostr_console.git * formatting + ok * clean up comments * add couple testcases * remove couple dependencies kepler: removed by adding Kepler class locally since the github repo is archived crypto: replace sha 256 hash with pointycastle equivalent * re-org * more re-org * remove `encrypt` dependency * message changes belong in separate PR * comment fix * leave comment on where kepler came from --- .gitignore | 3 + README.md | 1 + lib/nostr.dart | 1 + lib/src/crypto/kepler.dart | 95 ++++++++++++++++++++++++ lib/src/crypto/operator.dart | 119 +++++++++++++++++++++++++++++++ lib/src/event.dart | 70 ++++++++++++++++-- lib/src/nips/nip_004/crypto.dart | 111 ++++++++++++++++++++++++++++ lib/src/nips/nip_004/event.dart | 92 ++++++++++++++++++++++++ lib/src/settings.dart | 2 + pubspec.yaml | 1 - test/nips/nip_004_test.dart | 53 ++++++++++++++ 11 files changed, 540 insertions(+), 8 deletions(-) create mode 100644 lib/src/crypto/kepler.dart create mode 100644 lib/src/crypto/operator.dart create mode 100644 lib/src/nips/nip_004/crypto.dart create mode 100644 lib/src/nips/nip_004/event.dart create mode 100644 lib/src/settings.dart create mode 100644 test/nips/nip_004_test.dart diff --git a/.gitignore b/.gitignore index 65c34dc..f62a766 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,9 @@ # Files and directories created by pub. .dart_tool/ .packages +*.swp +x +tags # Conventional directory for build outputs. build/ diff --git a/README.md b/README.md index a5989bc..7cb4d4d 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,7 @@ flutter pub add nostr ## [NIPS](https://github.com/nostr-protocol/nips) - [x] [NIP 01 Basic protocol flow description](https://github.com/nostr-protocol/nips/blob/master/01.md) - [x] [NIP 02 Contact List and Petnames](https://github.com/nostr-protocol/nips/blob/master/02.md) +- [x] [NIP 04 Encrypted Direct Message](https://github.com/nostr-protocol/nips/blob/master/04.md) - [x] [NIP 15 End of Stored Events Notice](https://github.com/nostr-protocol/nips/blob/master/15.md) - [x] [NIP 20 Command Results](https://github.com/nostr-protocol/nips/blob/master/20.md) diff --git a/lib/nostr.dart b/lib/nostr.dart index bd86992..5bc4d95 100644 --- a/lib/nostr.dart +++ b/lib/nostr.dart @@ -11,3 +11,4 @@ export 'src/close.dart'; export 'src/message.dart'; export 'src/utils.dart'; export 'src/nips/nip_002.dart'; +export 'src/nips/nip_004/event.dart'; diff --git a/lib/src/crypto/kepler.dart b/lib/src/crypto/kepler.dart new file mode 100644 index 0000000..e55ea43 --- /dev/null +++ b/lib/src/crypto/kepler.dart @@ -0,0 +1,95 @@ +import 'package:convert/convert.dart'; +import 'dart:typed_data'; +import 'package:pointycastle/export.dart'; +import 'operator.dart'; + +/// +/// From archive repo: https://github.com/tjcampanella/kepler.git +/// + +class Kepler { + + /// return a Bytes data secret + static List> byteSecret(String privateString, String publicString) { + final secret = rawSecret(privateString, publicString); + assert(secret.x != null && secret.y != null); + final xs = secret.x!.toBigInteger()!.toRadixString(16); + final ys = secret.y!.toBigInteger()!.toRadixString(16); + final hexX = leftPadding(xs, 64); + final hexY = leftPadding(ys, 64); + final secretBytes = Uint8List.fromList(hex.decode('$hexX$hexY')); + final pair = [ + secretBytes.sublist(0, 32), + secretBytes.sublist(32, 40), + ]; + return pair; + } + + /// return a ECPoint data secret + static ECPoint rawSecret(String privateString, String publicString) { + final privateKey = loadPrivateKey(privateString); + final publicKey = loadPublicKey(publicString); + assert(privateKey.d != null && publicKey.Q != null); + final secret = scalarMultiple( + privateKey.d!, + publicKey.Q!, + ); + return secret; + } + + static String leftPadding(String s, int width) { + const paddingData = '000000000000000'; + final paddingWidth = width - s.length; + if (paddingWidth < 1) { + return s; + } + return "${paddingData.substring(0, paddingWidth)}$s"; + } + + static ECPoint scalarMultiple(BigInt k, ECPoint point) { + assert(isOnCurve(point)); + assert((k % theN).compareTo(BigInt.zero) != 0); + assert(point.x != null && point.y != null); + if (k < BigInt.from(0)) { + return scalarMultiple(-k, pointNeg(point)); + } + ECPoint? result; + ECPoint addend = point; + while (k > BigInt.from(0)) { + if (k & BigInt.from(1) > BigInt.from(0)) { + result = pointAdd(result, addend); + } + addend = pointAdd(addend, addend); + k >>= 1; + } + assert(isOnCurve(result!)); + return result!; + } + + /// return a privateKey from hex string + static ECPrivateKey loadPrivateKey(String storedkey) { + final d = BigInt.parse(storedkey, radix: 16); + final param = ECCurve_secp256k1(); + return ECPrivateKey(d, param); + } + + /// return a publicKey from hex string + static ECPublicKey loadPublicKey(String storedkey) { + final param = ECCurve_secp256k1(); + if (storedkey.length < 120) { + List codeList = []; + for (var _idx = 0; _idx < storedkey.length - 1; _idx += 2) { + final hexStr = storedkey.substring(_idx, _idx + 2); + codeList.add(int.parse(hexStr, radix: 16)); + } + final Q = param.curve.decodePoint(codeList); + return ECPublicKey(Q, param); + } else { + final x = BigInt.parse(storedkey.substring(0, 64), radix: 16); + final y = BigInt.parse(storedkey.substring(64), radix: 16); + final Q = param.curve.createPoint(x, y); + return ECPublicKey(Q, param); + } + } +} + diff --git a/lib/src/crypto/operator.dart b/lib/src/crypto/operator.dart new file mode 100644 index 0000000..f16b690 --- /dev/null +++ b/lib/src/crypto/operator.dart @@ -0,0 +1,119 @@ +import 'package:pointycastle/export.dart'; + +/// +/// From archive repo: https://github.com/tjcampanella/kepler.git +/// + +BigInt theP = BigInt.parse( + "fffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f", + radix: 16); +BigInt theN = BigInt.parse( + "fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141", + radix: 16); + +bool isOnCurve(ECPoint point) { + assert(point.x != null && + point.y != null && + point.curve.a != null && + point.curve.b != null); + final x = point.x!.toBigInteger(); + final y = point.y!.toBigInteger(); + final rs = (y! * y - + x! * x * x - + point.curve.a!.toBigInteger()! * x - + point.curve.b!.toBigInteger()!) % + theP; + return rs == BigInt.from(0); +} + +BigInt inverseMod(BigInt k, BigInt p) { + if (k.compareTo(BigInt.zero) == 0) { + throw Exception("Cannot Divide By 0"); + } + if (k < BigInt.from(0)) { + return p - inverseMod(-k, p); + } + var s = [BigInt.from(0), BigInt.from(1), BigInt.from(1)]; + var t = [BigInt.from(1), BigInt.from(0), BigInt.from(0)]; + var r = [p, k, k]; + while (r[0] != BigInt.from(0)) { + var quotient = r[2] ~/ r[0]; + r[1] = r[2] - quotient * r[0]; + r[2] = r[0]; + r[0] = r[1]; + s[1] = s[2] - quotient * s[0]; + s[2] = s[0]; + s[0] = s[1]; + t[1] = t[2] - quotient * t[0]; + t[2] = t[0]; + t[0] = t[1]; + } + final gcd = r[2]; + final x = s[2]; + // final y = t[2]; + assert(gcd == BigInt.from(1)); + assert((k * x) % p == BigInt.from(1)); + return x % p; +} + +ECPoint pointNeg(ECPoint point) { + assert(isOnCurve(point)); + assert(point.x != null || point.y != null); + final x = point.x!.toBigInteger(); + final y = point.y!.toBigInteger(); + final result = point.curve.createPoint(x!, -y! % theP); + assert(isOnCurve(result)); + return result; +} + +ECPoint pointAdd(ECPoint? point1, ECPoint? point2) { + if (point1 == null) { + return point2!; + } + if (point2 == null) { + return point1; + } + assert(isOnCurve(point1)); + assert(isOnCurve(point2)); + final x1 = point1.x!.toBigInteger(); + final y1 = point1.y!.toBigInteger(); + final x2 = point2.x!.toBigInteger(); + final y2 = point2.y!.toBigInteger(); + + // assert(x1 != x2 && y1 == y2); + // if (x1 == x2 && y1 != y2) { + // return null; + // } + BigInt m; + if (x1 == x2) { + m = (BigInt.from(3) * x1! * x1 + point1.curve.a!.toBigInteger()!) * + inverseMod(BigInt.from(2) * y1!, theP); + } else { + m = (y1! - y2!) * inverseMod(x1! - x2!, theP); + } + final x3 = m * m - x1 - x2!; + final y3 = y1 + m * (x3 - x1); + ECPoint result = point1.curve.createPoint(x3 % theP, -y3 % theP); + assert(isOnCurve(result)); + return result; +} + +ECPoint scalarMultiple(BigInt k, ECPoint point) { + assert(isOnCurve(point)); + assert((k % theN).compareTo(BigInt.zero) != 0); + assert(point.x != null && point.y != null); + if (k < BigInt.from(0)) { + return scalarMultiple(-k, pointNeg(point)); + } + ECPoint? result; + ECPoint addend = point; + while (k > BigInt.from(0)) { + if (k & BigInt.from(1) > BigInt.from(0)) { + result = pointAdd(result, addend); + } + addend = pointAdd(addend, addend); + k >>= 1; + } + assert(isOnCurve(result!)); + return result!; +} diff --git a/lib/src/event.dart b/lib/src/event.dart index 96dbb54..2571d84 100644 --- a/lib/src/event.dart +++ b/lib/src/event.dart @@ -1,8 +1,13 @@ import 'dart:convert'; -import 'package:convert/convert.dart'; -import 'package:crypto/crypto.dart'; +import 'dart:typed_data'; +import 'dart:math'; import 'package:bip340/bip340.dart' as bip340; -import 'package:nostr/src/utils.dart'; +import 'package:convert/convert.dart'; +import 'package:pointycastle/export.dart'; + +import 'nips/nip_004/crypto.dart'; +import 'utils.dart'; +import 'settings.dart'; /// The only object type that exists is the event, which has the following format on the wire: /// @@ -45,6 +50,11 @@ 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… @@ -253,11 +263,14 @@ class Event { throw Exception('invalid input'); } - var tags = (json['tags'] as List) + if (json['tags'] is String) { + json['tags'] = jsonDecode(json['tags']); + } + List> tags = (json['tags'] as List) .map((e) => (e as List).map((e) => e as String).toList()) .toList(); - return Event( + Event event = Event( json['id'], json['pubkey'], json['created_at'], @@ -268,6 +281,24 @@ 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. @@ -301,8 +332,8 @@ class Event { String content, ) { List data = [0, pubkey.toLowerCase(), createdAt, kind, tags, content]; - String serializedEvent = json.encode(data); - List hash = sha256.convert(utf8.encode(serializedEvent)).bytes; + String serializedEvent = jsonEncode(data); + Uint8List hash = SHA256Digest().process(Uint8List.fromList(utf8.encode(serializedEvent))); return hex.encode(hash); } @@ -326,6 +357,14 @@ 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 && @@ -335,4 +374,21 @@ 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; + } } diff --git a/lib/src/nips/nip_004/crypto.dart b/lib/src/nips/nip_004/crypto.dart new file mode 100644 index 0000000..5e3d6c8 --- /dev/null +++ b/lib/src/nips/nip_004/crypto.dart @@ -0,0 +1,111 @@ +import 'dart:convert'; +import 'dart:math'; +import 'dart:typed_data'; +import 'package:pointycastle/export.dart'; +import 'package:convert/convert.dart'; + +import '../../crypto/kepler.dart'; + + +class Nip04 { + static Map>> gMapByteSecret = {}; + + // 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]); + + // 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; + } + + // 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> byteSecret = gMapByteSecret[publicString]??[]; + if (byteSecret.isEmpty) { + byteSecret = Kepler.byteSecret(privateString, publicString); + gMapByteSecret[publicString] = byteSecret; + } + final secretIV = byteSecret; + final key = Uint8List.fromList(secretIV[0]); + final iv = b64IV.length > 6 + ? base64.decode(b64IV) + : 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 + + var offset = 0; + while (offset < cipherText.length - 16) { + offset += cipherImpl.processBlock(cipherText, offset, finalPlainText, offset); + } + //remove padding + offset += cipherImpl.doFinal(cipherText, offset, finalPlainText, offset); + return finalPlainText.sublist(0, offset); + } +} + diff --git a/lib/src/nips/nip_004/event.dart b/lib/src/nips/nip_004/event.dart new file mode 100644 index 0000000..3821626 --- /dev/null +++ b/lib/src/nips/nip_004/event.dart @@ -0,0 +1,92 @@ +import 'package:bip340/bip340.dart' as bip340; + +import '../../event.dart'; +import '../../utils.dart'; +import 'crypto.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; + } + + factory EncryptedDirectMessage.partial({ + peerPubkey = "", + id = "", + pubkey = "", + createdAt = 0, + kind = 4, + tags = const >[], + 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, + ); + } + + 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; + event.createdAt = currentUnixTimestampSeconds(); + event.pubkey = bip340.getPublicKey(privkey).toLowerCase(); + event.tags = [['p', peerPubkey],]; + event.peerPubkey = peerPubkey; + event.plaintext = plaintext; + if (referenceEventId != null) { + event.tags.add(['e', referenceEventId]); + } + event.id = event.getEventId(); + event.sig = event.getSignature(privkey); + return event; + } +} diff --git a/lib/src/settings.dart b/lib/src/settings.dart new file mode 100644 index 0000000..82efbed --- /dev/null +++ b/lib/src/settings.dart @@ -0,0 +1,2 @@ +String userPrivateKey = ""; +String userPublicKey = ""; diff --git a/pubspec.yaml b/pubspec.yaml index 8e9561c..6febabc 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -12,4 +12,3 @@ dev_dependencies: dependencies: bip340: ^0.1.0 convert: ^3.1.1 - crypto: ^3.0.2 diff --git a/test/nips/nip_004_test.dart b/test/nips/nip_004_test.dart new file mode 100644 index 0000000..d05b5f4 --- /dev/null +++ b/test/nips/nip_004_test.dart @@ -0,0 +1,53 @@ +import 'dart:convert'; + +import 'package:bip340/bip340.dart'; +import 'package:nostr/nostr.dart'; +import 'package:nostr/src/settings.dart'; +import 'package:test/test.dart'; + +void main() { + group('EncryptedDirectMessage', () { + test('EncryptedDirectMessage.newEvent', () { + // DM from bob to alice + String bobPubKey = "2d38a56c4303bc722370c50c86fc8dd3327f06a8fe59b3ff3d670738d71dd1e1"; + String alicePubKey = "0f76c800a7ea76b83a3ae87de94c6046b98311bda8885cedd8420885b50de181"; + String bobPrivKey = "826ef0e93c1278bd89945377fadb6b6b51d9eedf74ecdb64a96f1897bb670be8"; + String plaintext = "vi veri universum vivus vici"; + List> tags = [["p", alicePubKey]]; + + EncryptedDirectMessage event = EncryptedDirectMessage.newEvent( + alicePubKey, + plaintext, + bobPrivKey + ); + + expect(event.peerPubkey, alicePubKey); + expect(event.plaintext, plaintext); + expect(event.pubkey, bobPubKey); + expect(event.kind, 4); + expect(event.tags, tags); + expect(event.subscriptionId, null); + }); + }); + test('Encrypted DM Receive', () { + String bobPubKey = "2d38a56c4303bc722370c50c86fc8dd3327f06a8fe59b3ff3d670738d71dd1e1"; + String alicePubKey = "0f76c800a7ea76b83a3ae87de94c6046b98311bda8885cedd8420885b50de181"; + String bobPrivKey = "826ef0e93c1278bd89945377fadb6b6b51d9eedf74ecdb64a96f1897bb670be8"; + String json = '["EVENT", "181555e0fec2139d27ac80a5a46801415394e61d67be7f626631760dc5997bc0", {"id": "2739cccdc3fa943ad447378e234ef1325a76f023a169b483b6fe8cab47a793e1", "pubkey": "0f76c800a7ea76b83a3ae87de94c6046b98311bda8885cedd8420885b50de181", "created_at": 1680475069, "kind": 4, "tags": [["p", "2d38a56c4303bc722370c50c86fc8dd3327f06a8fe59b3ff3d670738d71dd1e1"]], "content": "hH1HlQWY3dz7IzJlgnEgW1WNtA0KlvGgo/OC4mep/R4I6PMqJuvZ35j4OFHkMvqb?iv=jbkXPH2esn5DIP3BodpsTQ==", "sig": "ba2d80d8be4612e8438447d38373e9b014b28172d49b7b6afeff462bc25bd4edc69c6e2c145dde052de54f47f4e36d74151fad3d08d21e3cad9563edc64adcca"}]'; + String tempPrivateKey = userPrivateKey; // from settings.dart + userPrivateKey = bobPrivKey; + + Message m = Message.deserialize(json); + Event event = m.message; + expect(event.id, "2739cccdc3fa943ad447378e234ef1325a76f023a169b483b6fe8cab47a793e1"); + expect(event.pubkey, alicePubKey); + expect(event.createdAt, 1680475069); + expect(event.kind, 4); + expect(event.tags, [["p", bobPubKey]]); + expect(event.content, "Secret message from alice to bob!"); + expect(event.sig, "ba2d80d8be4612e8438447d38373e9b014b28172d49b7b6afeff462bc25bd4edc69c6e2c145dde052de54f47f4e36d74151fad3d08d21e3cad9563edc64adcca"); + expect(event.subscriptionId, "181555e0fec2139d27ac80a5a46801415394e61d67be7f626631760dc5997bc0"); + + userPrivateKey = tempPrivateKey; + }); +} From 72545f599a07c78b129832403658a9dbd76f6f5c Mon Sep 17 00:00:00 2001 From: ethicnology Date: Sun, 16 Apr 2023 19:13:34 +0200 Subject: [PATCH 02/16] fix: #25 warnings --- lib/src/crypto/kepler.dart | 6 +-- lib/src/event.dart | 10 ++-- lib/src/nips/nip_004/crypto.dart | 83 ++++++++++++++++---------------- lib/src/nips/nip_004/event.dart | 29 +++++------ pubspec.yaml | 2 + test/nips/nip_004_test.dart | 48 ++++++++++-------- 6 files changed, 94 insertions(+), 84 deletions(-) diff --git a/lib/src/crypto/kepler.dart b/lib/src/crypto/kepler.dart index e55ea43..dc9d976 100644 --- a/lib/src/crypto/kepler.dart +++ b/lib/src/crypto/kepler.dart @@ -8,7 +8,6 @@ import 'operator.dart'; /// class Kepler { - /// return a Bytes data secret static List> byteSecret(String privateString, String publicString) { final secret = rawSecret(privateString, publicString); @@ -78,8 +77,8 @@ class Kepler { final param = ECCurve_secp256k1(); if (storedkey.length < 120) { List codeList = []; - for (var _idx = 0; _idx < storedkey.length - 1; _idx += 2) { - final hexStr = storedkey.substring(_idx, _idx + 2); + for (var idx = 0; idx < storedkey.length - 1; idx += 2) { + final hexStr = storedkey.substring(idx, idx + 2); codeList.add(int.parse(hexStr, radix: 16)); } final Q = param.curve.decodePoint(codeList); @@ -92,4 +91,3 @@ class Kepler { } } } - diff --git a/lib/src/event.dart b/lib/src/event.dart index 2571d84..41a6ea2 100644 --- a/lib/src/event.dart +++ b/lib/src/event.dart @@ -1,6 +1,5 @@ import 'dart:convert'; import 'dart:typed_data'; -import 'dart:math'; import 'package:bip340/bip340.dart' as bip340; import 'package:convert/convert.dart'; import 'package:pointycastle/export.dart'; @@ -333,7 +332,8 @@ class Event { ) { List data = [0, pubkey.toLowerCase(), createdAt, kind, tags, content]; String serializedEvent = jsonEncode(data); - Uint8List hash = SHA256Digest().process(Uint8List.fromList(utf8.encode(serializedEvent))); + Uint8List hash = SHA256Digest() + .process(Uint8List.fromList(utf8.encode(serializedEvent))); return hex.encode(hash); } @@ -377,16 +377,16 @@ class Event { bool nip04Decrypt() { int ivIndex = content.indexOf("?iv="); - if( ivIndex <= 0) { + 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); + content = Nip04.decrypt(userPrivateKey, "02$pubkey", encString, iv); decrypted = true; - } catch(e) { + } catch (e) { //print("Fail to decrypt: ${e}"); } return decrypted; diff --git a/lib/src/nips/nip_004/crypto.dart b/lib/src/nips/nip_004/crypto.dart index 5e3d6c8..a509fef 100644 --- a/lib/src/nips/nip_004/crypto.dart +++ b/lib/src/nips/nip_004/crypto.dart @@ -2,60 +2,62 @@ import 'dart:convert'; import 'dart:math'; import 'dart:typed_data'; import 'package:pointycastle/export.dart'; -import 'package:convert/convert.dart'; import '../../crypto/kepler.dart'; - class Nip04 { static Map>> gMapByteSecret = {}; // Encrypt data using self private key in nostr format ( with trailing ?iv=) - static String encryptMessage( String privateString, - String publicString, - String plainText) { + static String encryptMessage( + String privateString, String publicString, String plainText) { Uint8List uintInputText = Utf8Encoder().convert(plainText); - final encryptedString = encryptMessageRaw(privateString, publicString, uintInputText); + final encryptedString = + encryptMessageRaw(privateString, publicString, uintInputText); return encryptedString; } - static String encryptMessageRaw( String privateString, - String publicString, - Uint8List uintInputText) { + static String encryptMessageRaw( + String privateString, String publicString, Uint8List uintInputText) { final secretIV = Kepler.byteSecret(privateString, publicString); final key = Uint8List.fromList(secretIV[0]); // generate iv https://stackoverflow.com/questions/63630661/aes-engine-not-initialised-with-pointycastle-securerandom FortunaRandom fr = FortunaRandom(); - final _sGen = Random.secure(); + final sGen = Random.secure(); fr.seed(KeyParameter( - Uint8List.fromList(List.generate(32, (_) => _sGen.nextInt(255))))); + Uint8List.fromList(List.generate(32, (_) => sGen.nextInt(255))))); final iv = fr.nextBytes(16); - CipherParameters params = PaddedBlockCipherParameters(ParametersWithIV(KeyParameter(key), iv), null); + CipherParameters params = PaddedBlockCipherParameters( + ParametersWithIV(KeyParameter(key), iv), null); - PaddedBlockCipherImpl cipherImpl = PaddedBlockCipherImpl(PKCS7Padding(), CBCBlockCipher(AESEngine())); + PaddedBlockCipherImpl cipherImpl = + PaddedBlockCipherImpl(PKCS7Padding(), CBCBlockCipher(AESEngine())); - cipherImpl.init(true, // means to encrypt - params as PaddedBlockCipherParameters); + 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); + offset += cipherImpl.processBlock( + uintInputText, offset, outputEncodedText, offset); } - //add padding - offset += cipherImpl.doFinal(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; + outputPlainText = "$outputPlainText?iv=$stringIv"; + return outputPlainText; } // pointy castle source https://github.com/PointyCastle/pointycastle/blob/master/tutorials/aes-cbc.md @@ -63,21 +65,18 @@ class Nip04 { // 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 = ""]) { - + 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> byteSecret = gMapByteSecret[publicString]??[]; + static Uint8List decryptRaw( + String privateString, String publicString, Uint8List cipherText, + [String b64IV = ""]) { + List> byteSecret = gMapByteSecret[publicString] ?? []; if (byteSecret.isEmpty) { byteSecret = Kepler.byteSecret(privateString, publicString); gMapByteSecret[publicString] = byteSecret; @@ -85,27 +84,29 @@ class Nip04 { final secretIV = byteSecret; final key = Uint8List.fromList(secretIV[0]); final iv = b64IV.length > 6 - ? base64.decode(b64IV) - : Uint8List.fromList(secretIV[1]); + ? base64.decode(b64IV) + : Uint8List.fromList(secretIV[1]); CipherParameters params = PaddedBlockCipherParameters( ParametersWithIV(KeyParameter(key), iv), null); - PaddedBlockCipherImpl cipherImpl = PaddedBlockCipherImpl( - PKCS7Padding(), CBCBlockCipher(AESEngine())); + PaddedBlockCipherImpl cipherImpl = + PaddedBlockCipherImpl(PKCS7Padding(), CBCBlockCipher(AESEngine())); - cipherImpl.init(false, - params as PaddedBlockCipherParameters); - final Uint8List finalPlainText = Uint8List(cipherText.length); // allocate space + cipherImpl.init( + false, + params as PaddedBlockCipherParameters); + final Uint8List finalPlainText = + Uint8List(cipherText.length); // allocate space var offset = 0; while (offset < cipherText.length - 16) { - offset += cipherImpl.processBlock(cipherText, offset, finalPlainText, offset); + offset += + cipherImpl.processBlock(cipherText, offset, finalPlainText, offset); } //remove padding offset += cipherImpl.doFinal(cipherText, offset, finalPlainText, offset); return finalPlainText.sublist(0, offset); } } - diff --git a/lib/src/nips/nip_004/event.dart b/lib/src/nips/nip_004/event.dart index 3821626..9cced90 100644 --- a/lib/src/nips/nip_004/event.dart +++ b/lib/src/nips/nip_004/event.dart @@ -4,9 +4,8 @@ import '../../event.dart'; import '../../utils.dart'; import 'crypto.dart'; - class EncryptedDirectMessage extends Event { - late String peerPubkey; + late String peerPubkey; late String? plaintext; late String? referenceEventId; @@ -24,16 +23,16 @@ class EncryptedDirectMessage extends Event { this.plaintext, this.referenceEventId, }) : super( - id, - pubkey, - createdAt, - kind, - tags, - content, - sig, - subscriptionId: subscriptionId, - verify: verify, - ) { + id, + pubkey, + createdAt, + kind, + tags, + content, + sig, + subscriptionId: subscriptionId, + verify: verify, + ) { kind = 4; plaintext = content; } @@ -75,11 +74,13 @@ class EncryptedDirectMessage extends Event { String? referenceEventId, }) { EncryptedDirectMessage event = EncryptedDirectMessage.partial(); - event.content = Nip04.encryptMessage(privkey, '02' + peerPubkey, plaintext); + event.content = Nip04.encryptMessage(privkey, '02$peerPubkey', plaintext); event.kind = 4; event.createdAt = currentUnixTimestampSeconds(); event.pubkey = bip340.getPublicKey(privkey).toLowerCase(); - event.tags = [['p', peerPubkey],]; + event.tags = [ + ['p', peerPubkey], + ]; event.peerPubkey = peerPubkey; event.plaintext = plaintext; if (referenceEventId != null) { diff --git a/pubspec.yaml b/pubspec.yaml index 6febabc..088a90d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -12,3 +12,5 @@ dev_dependencies: dependencies: bip340: ^0.1.0 convert: ^3.1.1 + pointycastle: ^3.7.3 + \ No newline at end of file diff --git a/test/nips/nip_004_test.dart b/test/nips/nip_004_test.dart index d05b5f4..8d3736d 100644 --- a/test/nips/nip_004_test.dart +++ b/test/nips/nip_004_test.dart @@ -1,6 +1,3 @@ -import 'dart:convert'; - -import 'package:bip340/bip340.dart'; import 'package:nostr/nostr.dart'; import 'package:nostr/src/settings.dart'; import 'package:test/test.dart'; @@ -9,17 +6,19 @@ void main() { group('EncryptedDirectMessage', () { test('EncryptedDirectMessage.newEvent', () { // DM from bob to alice - String bobPubKey = "2d38a56c4303bc722370c50c86fc8dd3327f06a8fe59b3ff3d670738d71dd1e1"; - String alicePubKey = "0f76c800a7ea76b83a3ae87de94c6046b98311bda8885cedd8420885b50de181"; - String bobPrivKey = "826ef0e93c1278bd89945377fadb6b6b51d9eedf74ecdb64a96f1897bb670be8"; + String bobPubKey = + "2d38a56c4303bc722370c50c86fc8dd3327f06a8fe59b3ff3d670738d71dd1e1"; + String alicePubKey = + "0f76c800a7ea76b83a3ae87de94c6046b98311bda8885cedd8420885b50de181"; + String bobPrivKey = + "826ef0e93c1278bd89945377fadb6b6b51d9eedf74ecdb64a96f1897bb670be8"; String plaintext = "vi veri universum vivus vici"; - List> tags = [["p", alicePubKey]]; + List> tags = [ + ["p", alicePubKey] + ]; - EncryptedDirectMessage event = EncryptedDirectMessage.newEvent( - alicePubKey, - plaintext, - bobPrivKey - ); + EncryptedDirectMessage event = + EncryptedDirectMessage.newEvent(alicePubKey, plaintext, bobPrivKey); expect(event.peerPubkey, alicePubKey); expect(event.plaintext, plaintext); @@ -30,23 +29,32 @@ void main() { }); }); test('Encrypted DM Receive', () { - String bobPubKey = "2d38a56c4303bc722370c50c86fc8dd3327f06a8fe59b3ff3d670738d71dd1e1"; - String alicePubKey = "0f76c800a7ea76b83a3ae87de94c6046b98311bda8885cedd8420885b50de181"; - String bobPrivKey = "826ef0e93c1278bd89945377fadb6b6b51d9eedf74ecdb64a96f1897bb670be8"; - String json = '["EVENT", "181555e0fec2139d27ac80a5a46801415394e61d67be7f626631760dc5997bc0", {"id": "2739cccdc3fa943ad447378e234ef1325a76f023a169b483b6fe8cab47a793e1", "pubkey": "0f76c800a7ea76b83a3ae87de94c6046b98311bda8885cedd8420885b50de181", "created_at": 1680475069, "kind": 4, "tags": [["p", "2d38a56c4303bc722370c50c86fc8dd3327f06a8fe59b3ff3d670738d71dd1e1"]], "content": "hH1HlQWY3dz7IzJlgnEgW1WNtA0KlvGgo/OC4mep/R4I6PMqJuvZ35j4OFHkMvqb?iv=jbkXPH2esn5DIP3BodpsTQ==", "sig": "ba2d80d8be4612e8438447d38373e9b014b28172d49b7b6afeff462bc25bd4edc69c6e2c145dde052de54f47f4e36d74151fad3d08d21e3cad9563edc64adcca"}]'; + String bobPubKey = + "2d38a56c4303bc722370c50c86fc8dd3327f06a8fe59b3ff3d670738d71dd1e1"; + String alicePubKey = + "0f76c800a7ea76b83a3ae87de94c6046b98311bda8885cedd8420885b50de181"; + String bobPrivKey = + "826ef0e93c1278bd89945377fadb6b6b51d9eedf74ecdb64a96f1897bb670be8"; + String json = + '["EVENT", "181555e0fec2139d27ac80a5a46801415394e61d67be7f626631760dc5997bc0", {"id": "2739cccdc3fa943ad447378e234ef1325a76f023a169b483b6fe8cab47a793e1", "pubkey": "0f76c800a7ea76b83a3ae87de94c6046b98311bda8885cedd8420885b50de181", "created_at": 1680475069, "kind": 4, "tags": [["p", "2d38a56c4303bc722370c50c86fc8dd3327f06a8fe59b3ff3d670738d71dd1e1"]], "content": "hH1HlQWY3dz7IzJlgnEgW1WNtA0KlvGgo/OC4mep/R4I6PMqJuvZ35j4OFHkMvqb?iv=jbkXPH2esn5DIP3BodpsTQ==", "sig": "ba2d80d8be4612e8438447d38373e9b014b28172d49b7b6afeff462bc25bd4edc69c6e2c145dde052de54f47f4e36d74151fad3d08d21e3cad9563edc64adcca"}]'; String tempPrivateKey = userPrivateKey; // from settings.dart userPrivateKey = bobPrivKey; Message m = Message.deserialize(json); Event event = m.message; - expect(event.id, "2739cccdc3fa943ad447378e234ef1325a76f023a169b483b6fe8cab47a793e1"); + expect(event.id, + "2739cccdc3fa943ad447378e234ef1325a76f023a169b483b6fe8cab47a793e1"); expect(event.pubkey, alicePubKey); expect(event.createdAt, 1680475069); expect(event.kind, 4); - expect(event.tags, [["p", bobPubKey]]); + expect(event.tags, [ + ["p", bobPubKey] + ]); expect(event.content, "Secret message from alice to bob!"); - expect(event.sig, "ba2d80d8be4612e8438447d38373e9b014b28172d49b7b6afeff462bc25bd4edc69c6e2c145dde052de54f47f4e36d74151fad3d08d21e3cad9563edc64adcca"); - expect(event.subscriptionId, "181555e0fec2139d27ac80a5a46801415394e61d67be7f626631760dc5997bc0"); + expect(event.sig, + "ba2d80d8be4612e8438447d38373e9b014b28172d49b7b6afeff462bc25bd4edc69c6e2c145dde052de54f47f4e36d74151fad3d08d21e3cad9563edc64adcca"); + expect(event.subscriptionId, + "181555e0fec2139d27ac80a5a46801415394e61d67be7f626631760dc5997bc0"); userPrivateKey = tempPrivateKey; }); From f2f1a0130217a3b031ba6408f3f71c19d5c4e8d6 Mon Sep 17 00:00:00 2001 From: ethicnology Date: Sun, 16 Apr 2023 23:01:53 +0200 Subject: [PATCH 03/16] refactor: PR #25 --- lib/src/event.dart | 64 ++------------- lib/src/message.dart | 1 + lib/src/nips/nip_004/crypto.dart | 64 +++++++-------- lib/src/nips/nip_004/event.dart | 130 ++++++++++++------------------- lib/src/settings.dart | 2 - test/nips/nip_004_test.dart | 100 ++++++++++++------------ 6 files changed, 132 insertions(+), 229 deletions(-) delete mode 100644 lib/src/settings.dart diff --git a/lib/src/event.dart b/lib/src/event.dart index 41a6ea2..31e8849 100644 --- a/lib/src/event.dart +++ b/lib/src/event.dart @@ -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: /// @@ -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… @@ -262,14 +254,11 @@ class Event { throw Exception('invalid input'); } - if (json['tags'] is String) { - json['tags'] = jsonDecode(json['tags']); - } - List> tags = (json['tags'] as List) + var tags = (json['tags'] as List) .map((e) => (e as List).map((e) => e as String).toList()) .toList(); - Event event = Event( + return Event( json['id'], json['pubkey'], json['created_at'], @@ -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. @@ -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); @@ -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 && @@ -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; - } } diff --git a/lib/src/message.dart b/lib/src/message.dart index 9d3d0c2..7cdfc00 100644 --- a/lib/src/message.dart +++ b/lib/src/message.dart @@ -17,6 +17,7 @@ class Message { switch (type) { case "EVENT": message = Event.deserialize(data); + if (message.kind == 4) message = EncryptedDirectMessage(message); break; case "REQ": message = Request.deserialize(data); diff --git a/lib/src/nips/nip_004/crypto.dart b/lib/src/nips/nip_004/crypto.dart index a509fef..6692ba3 100644 --- a/lib/src/nips/nip_004/crypto.dart +++ b/lib/src/nips/nip_004/crypto.dart @@ -5,22 +5,23 @@ import 'package:pointycastle/export.dart'; import '../../crypto/kepler.dart'; -class Nip04 { +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 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(); @@ -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> 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> 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( @@ -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()); } } diff --git a/lib/src/nips/nip_004/event.dart b/lib/src/nips/nip_004/event.dart index 9cced90..b2f470f 100644 --- a/lib/src/nips/nip_004/event.dart +++ b/lib/src/nips/nip_004/event.dart @@ -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>> gMapByteSecret = {}; - factory EncryptedDirectMessage.partial({ - peerPubkey = "", - id = "", - pubkey = "", - createdAt = 0, - kind = 4, - tags = const >[], - 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) { + return Nip4.decipher(receiverPrivkey, senderPubkey, content); + } + + String? findPubkey() { + String prefix = "p"; + for (List tag in tags) { + if (tag.isNotEmpty && tag[0] == prefix && tag.length > 1) return tag[1]; + } + return null; } } diff --git a/lib/src/settings.dart b/lib/src/settings.dart deleted file mode 100644 index 82efbed..0000000 --- a/lib/src/settings.dart +++ /dev/null @@ -1,2 +0,0 @@ -String userPrivateKey = ""; -String userPublicKey = ""; diff --git a/test/nips/nip_004_test.dart b/test/nips/nip_004_test.dart index 8d3736d..e0991f8 100644 --- a/test/nips/nip_004_test.dart +++ b/test/nips/nip_004_test.dart @@ -1,61 +1,59 @@ -import 'package:nostr/nostr.dart'; -import 'package:nostr/src/settings.dart'; import 'package:test/test.dart'; void main() { group('EncryptedDirectMessage', () { - test('EncryptedDirectMessage.newEvent', () { - // DM from bob to alice - String bobPubKey = - "2d38a56c4303bc722370c50c86fc8dd3327f06a8fe59b3ff3d670738d71dd1e1"; - String alicePubKey = - "0f76c800a7ea76b83a3ae87de94c6046b98311bda8885cedd8420885b50de181"; - String bobPrivKey = - "826ef0e93c1278bd89945377fadb6b6b51d9eedf74ecdb64a96f1897bb670be8"; - String plaintext = "vi veri universum vivus vici"; - List> tags = [ - ["p", alicePubKey] - ]; + // test('EncryptedDirectMessage.newEvent', () { + // // DM from bob to alice + // String bobPubKey = + // "2d38a56c4303bc722370c50c86fc8dd3327f06a8fe59b3ff3d670738d71dd1e1"; + // String alicePubKey = + // "0f76c800a7ea76b83a3ae87de94c6046b98311bda8885cedd8420885b50de181"; + // String bobPrivKey = + // "826ef0e93c1278bd89945377fadb6b6b51d9eedf74ecdb64a96f1897bb670be8"; + // String plaintext = "vi veri universum vivus vici"; + // List> tags = [ + // ["p", alicePubKey] + // ]; - EncryptedDirectMessage event = - EncryptedDirectMessage.newEvent(alicePubKey, plaintext, bobPrivKey); + // EncryptedDirectMessage event = + // EncryptedDirectMessage.newEvent(alicePubKey, plaintext, bobPrivKey); - expect(event.peerPubkey, alicePubKey); - expect(event.plaintext, plaintext); - expect(event.pubkey, bobPubKey); - expect(event.kind, 4); - expect(event.tags, tags); - expect(event.subscriptionId, null); - }); - }); - test('Encrypted DM Receive', () { - String bobPubKey = - "2d38a56c4303bc722370c50c86fc8dd3327f06a8fe59b3ff3d670738d71dd1e1"; - String alicePubKey = - "0f76c800a7ea76b83a3ae87de94c6046b98311bda8885cedd8420885b50de181"; - String bobPrivKey = - "826ef0e93c1278bd89945377fadb6b6b51d9eedf74ecdb64a96f1897bb670be8"; - String json = - '["EVENT", "181555e0fec2139d27ac80a5a46801415394e61d67be7f626631760dc5997bc0", {"id": "2739cccdc3fa943ad447378e234ef1325a76f023a169b483b6fe8cab47a793e1", "pubkey": "0f76c800a7ea76b83a3ae87de94c6046b98311bda8885cedd8420885b50de181", "created_at": 1680475069, "kind": 4, "tags": [["p", "2d38a56c4303bc722370c50c86fc8dd3327f06a8fe59b3ff3d670738d71dd1e1"]], "content": "hH1HlQWY3dz7IzJlgnEgW1WNtA0KlvGgo/OC4mep/R4I6PMqJuvZ35j4OFHkMvqb?iv=jbkXPH2esn5DIP3BodpsTQ==", "sig": "ba2d80d8be4612e8438447d38373e9b014b28172d49b7b6afeff462bc25bd4edc69c6e2c145dde052de54f47f4e36d74151fad3d08d21e3cad9563edc64adcca"}]'; - String tempPrivateKey = userPrivateKey; // from settings.dart - userPrivateKey = bobPrivKey; + // expect(event.peerPubkey, alicePubKey); + // expect(event.plaintext, plaintext); + // expect(event.pubkey, bobPubKey); + // expect(event.kind, 4); + // expect(event.tags, tags); + // expect(event.subscriptionId, null); + // }); + // }); + // test('Encrypted DM Receive', () { + // String bobPubKey = + // "2d38a56c4303bc722370c50c86fc8dd3327f06a8fe59b3ff3d670738d71dd1e1"; + // String alicePubKey = + // "0f76c800a7ea76b83a3ae87de94c6046b98311bda8885cedd8420885b50de181"; + // String bobPrivKey = + // "826ef0e93c1278bd89945377fadb6b6b51d9eedf74ecdb64a96f1897bb670be8"; + // String json = + // '["EVENT", "181555e0fec2139d27ac80a5a46801415394e61d67be7f626631760dc5997bc0", {"id": "2739cccdc3fa943ad447378e234ef1325a76f023a169b483b6fe8cab47a793e1", "pubkey": "0f76c800a7ea76b83a3ae87de94c6046b98311bda8885cedd8420885b50de181", "created_at": 1680475069, "kind": 4, "tags": [["p", "2d38a56c4303bc722370c50c86fc8dd3327f06a8fe59b3ff3d670738d71dd1e1"]], "content": "hH1HlQWY3dz7IzJlgnEgW1WNtA0KlvGgo/OC4mep/R4I6PMqJuvZ35j4OFHkMvqb?iv=jbkXPH2esn5DIP3BodpsTQ==", "sig": "ba2d80d8be4612e8438447d38373e9b014b28172d49b7b6afeff462bc25bd4edc69c6e2c145dde052de54f47f4e36d74151fad3d08d21e3cad9563edc64adcca"}]'; - Message m = Message.deserialize(json); - Event event = m.message; - expect(event.id, - "2739cccdc3fa943ad447378e234ef1325a76f023a169b483b6fe8cab47a793e1"); - expect(event.pubkey, alicePubKey); - expect(event.createdAt, 1680475069); - expect(event.kind, 4); - expect(event.tags, [ - ["p", bobPubKey] - ]); - expect(event.content, "Secret message from alice to bob!"); - expect(event.sig, - "ba2d80d8be4612e8438447d38373e9b014b28172d49b7b6afeff462bc25bd4edc69c6e2c145dde052de54f47f4e36d74151fad3d08d21e3cad9563edc64adcca"); - expect(event.subscriptionId, - "181555e0fec2139d27ac80a5a46801415394e61d67be7f626631760dc5997bc0"); + // // String tempPrivateKey = userPrivateKey; // from settings.dart + // // userPrivateKey = bobPrivKey; + // // userPrivateKey = tempPrivateKey; - userPrivateKey = tempPrivateKey; + // Message m = Message.deserialize(json); + // Event event = m.message; + // expect(event.id, + // "2739cccdc3fa943ad447378e234ef1325a76f023a169b483b6fe8cab47a793e1"); + // expect(event.pubkey, alicePubKey); + // expect(event.createdAt, 1680475069); + // expect(event.kind, 4); + // expect(event.tags, [ + // ["p", bobPubKey] + // ]); + // expect(event.content, "Secret message from alice to bob!"); + // expect(event.sig, + // "ba2d80d8be4612e8438447d38373e9b014b28172d49b7b6afeff462bc25bd4edc69c6e2c145dde052de54f47f4e36d74151fad3d08d21e3cad9563edc64adcca"); + // expect(event.subscriptionId, + // "181555e0fec2139d27ac80a5a46801415394e61d67be7f626631760dc5997bc0"); }); } From 43b412b18727f0b916e13e9fcde5c79290be9b5c Mon Sep 17 00:00:00 2001 From: no-prob <113266379+no-prob@users.noreply.github.com> Date: Wed, 19 Apr 2023 23:38:00 -0700 Subject: [PATCH 04/16] fix: decipher bug, add Event.quick (#26) * fix encrypt bug, add Event.quick * a send and a receive unit test --- lib/src/event.dart | 16 +++++- lib/src/nips/nip_004/crypto.dart | 3 +- lib/src/nips/nip_004/event.dart | 20 ++++++- test/nips/nip_004_test.dart | 94 +++++++++++++++----------------- 4 files changed, 79 insertions(+), 54 deletions(-) diff --git a/lib/src/event.dart b/lib/src/event.dart index 31e8849..2748f4b 100644 --- a/lib/src/event.dart +++ b/lib/src/event.dart @@ -254,7 +254,7 @@ class Event { throw Exception('invalid input'); } - var tags = (json['tags'] as List) + List> tags = (json['tags'] as List) .map((e) => (e as List).map((e) => e as String).toList()) .toList(); @@ -271,6 +271,20 @@ 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 6692ba3..f342b87 100644 --- a/lib/src/nips/nip_004/crypto.dart +++ b/lib/src/nips/nip_004/crypto.dart @@ -61,7 +61,6 @@ class Nip4 { return outputPlainText; } - // Decrypt data using self private key static String decipher( String privkey, String pubkey, @@ -101,6 +100,6 @@ class Nip4 { //remove padding offset += cipherImpl.doFinal(cipherText, offset, finalPlainText, offset); Uint8List result = finalPlainText.sublist(0, offset); - return Utf8Decoder().convert(result.toList()); + return Utf8Decoder().convert(result); } } diff --git a/lib/src/nips/nip_004/event.dart b/lib/src/nips/nip_004/event.dart index b2f470f..f437f03 100644 --- a/lib/src/nips/nip_004/event.dart +++ b/lib/src/nips/nip_004/event.dart @@ -45,8 +45,24 @@ class EncryptedDirectMessage extends Event { return ciphertext; } - String getPlaintext(String receiverPrivkey, String senderPubkey) { - return Nip4.decipher(receiverPrivkey, senderPubkey, content); + String getPlaintext(String receiverPrivkey, [String senderPubkey=""]) { + if (senderPubkey.length == 0) { + senderPubkey = pubkey; + } + 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}"); + } + return plaintext; } String? findPubkey() { diff --git a/test/nips/nip_004_test.dart b/test/nips/nip_004_test.dart index e0991f8..f10f075 100644 --- a/test/nips/nip_004_test.dart +++ b/test/nips/nip_004_test.dart @@ -1,59 +1,55 @@ +import 'package:nostr/nostr.dart'; import 'package:test/test.dart'; +String bobPubkey = + "2d38a56c4303bc722370c50c86fc8dd3327f06a8fe59b3ff3d670738d71dd1e1"; +String alicePubkey = + "0f76c800a7ea76b83a3ae87de94c6046b98311bda8885cedd8420885b50de181"; +String bobPrivkey = + "826ef0e93c1278bd89945377fadb6b6b51d9eedf74ecdb64a96f1897bb670be8"; +String alicePrivkey = + "773dc29ff81f7680eeca5d530f528e8c572979b46abc8bfd1586b73a6a98ab4d"; + void main() { group('EncryptedDirectMessage', () { - // test('EncryptedDirectMessage.newEvent', () { - // // DM from bob to alice - // String bobPubKey = - // "2d38a56c4303bc722370c50c86fc8dd3327f06a8fe59b3ff3d670738d71dd1e1"; - // String alicePubKey = - // "0f76c800a7ea76b83a3ae87de94c6046b98311bda8885cedd8420885b50de181"; - // String bobPrivKey = - // "826ef0e93c1278bd89945377fadb6b6b51d9eedf74ecdb64a96f1897bb670be8"; - // String plaintext = "vi veri universum vivus vici"; - // List> tags = [ - // ["p", alicePubKey] - // ]; + test('EncryptedDirectMessage.quick', () { + // DM from bob to alice + String plaintext = "vi veri universum vivus vici"; + List> tags = [ + ['p', alicePubkey] + ]; - // EncryptedDirectMessage event = - // EncryptedDirectMessage.newEvent(alicePubKey, plaintext, bobPrivKey); + EncryptedDirectMessage event = + EncryptedDirectMessage.quick(bobPrivkey, alicePubkey, plaintext); - // expect(event.peerPubkey, alicePubKey); - // expect(event.plaintext, plaintext); - // expect(event.pubkey, bobPubKey); - // expect(event.kind, 4); - // expect(event.tags, tags); - // expect(event.subscriptionId, null); - // }); - // }); - // test('Encrypted DM Receive', () { - // String bobPubKey = - // "2d38a56c4303bc722370c50c86fc8dd3327f06a8fe59b3ff3d670738d71dd1e1"; - // String alicePubKey = - // "0f76c800a7ea76b83a3ae87de94c6046b98311bda8885cedd8420885b50de181"; - // String bobPrivKey = - // "826ef0e93c1278bd89945377fadb6b6b51d9eedf74ecdb64a96f1897bb670be8"; - // String json = - // '["EVENT", "181555e0fec2139d27ac80a5a46801415394e61d67be7f626631760dc5997bc0", {"id": "2739cccdc3fa943ad447378e234ef1325a76f023a169b483b6fe8cab47a793e1", "pubkey": "0f76c800a7ea76b83a3ae87de94c6046b98311bda8885cedd8420885b50de181", "created_at": 1680475069, "kind": 4, "tags": [["p", "2d38a56c4303bc722370c50c86fc8dd3327f06a8fe59b3ff3d670738d71dd1e1"]], "content": "hH1HlQWY3dz7IzJlgnEgW1WNtA0KlvGgo/OC4mep/R4I6PMqJuvZ35j4OFHkMvqb?iv=jbkXPH2esn5DIP3BodpsTQ==", "sig": "ba2d80d8be4612e8438447d38373e9b014b28172d49b7b6afeff462bc25bd4edc69c6e2c145dde052de54f47f4e36d74151fad3d08d21e3cad9563edc64adcca"}]'; + expect(event.receiverPubkey, alicePubkey); + expect(event.getPlaintext(alicePrivkey), plaintext); + expect(event.pubkey, bobPubkey); + expect(event.kind, 4); + expect(event.tags, tags); + expect(event.subscriptionId, null); + }); - // // String tempPrivateKey = userPrivateKey; // from settings.dart - // // userPrivateKey = bobPrivKey; - // // userPrivateKey = tempPrivateKey; + test('EncryptedDirectMessage Receive', () { + String receivedEvent = + '["EVENT", "181555e0fec2139d27ac80a5a46801415394e61d67be7f626631760dc5997bc0", {"id": "2739cccdc3fa943ad447378e234ef1325a76f023a169b483b6fe8cab47a793e1", "pubkey": "0f76c800a7ea76b83a3ae87de94c6046b98311bda8885cedd8420885b50de181", "created_at": 1680475069, "kind": 4, "tags": [["p", "2d38a56c4303bc722370c50c86fc8dd3327f06a8fe59b3ff3d670738d71dd1e1"]], "content": "hH1HlQWY3dz7IzJlgnEgW1WNtA0KlvGgo/OC4mep/R4I6PMqJuvZ35j4OFHkMvqb?iv=jbkXPH2esn5DIP3BodpsTQ==", "sig": "ba2d80d8be4612e8438447d38373e9b014b28172d49b7b6afeff462bc25bd4edc69c6e2c145dde052de54f47f4e36d74151fad3d08d21e3cad9563edc64adcca"}]'; - // Message m = Message.deserialize(json); - // Event event = m.message; - // expect(event.id, - // "2739cccdc3fa943ad447378e234ef1325a76f023a169b483b6fe8cab47a793e1"); - // expect(event.pubkey, alicePubKey); - // expect(event.createdAt, 1680475069); - // expect(event.kind, 4); - // expect(event.tags, [ - // ["p", bobPubKey] - // ]); - // expect(event.content, "Secret message from alice to bob!"); - // expect(event.sig, - // "ba2d80d8be4612e8438447d38373e9b014b28172d49b7b6afeff462bc25bd4edc69c6e2c145dde052de54f47f4e36d74151fad3d08d21e3cad9563edc64adcca"); - // expect(event.subscriptionId, - // "181555e0fec2139d27ac80a5a46801415394e61d67be7f626631760dc5997bc0"); + Message m = Message.deserialize(receivedEvent); + Event event = m.message; + expect(event.id, + "2739cccdc3fa943ad447378e234ef1325a76f023a169b483b6fe8cab47a793e1"); + expect(event.pubkey, alicePubkey); + expect(event.createdAt, 1680475069); + expect(event.kind, 4); + expect(event.tags, [ + ["p", bobPubkey] + ]); + String content = (event as EncryptedDirectMessage).getPlaintext(bobPrivkey); + expect(content, "Secret message from alice to bob!"); + expect(event.sig, + "ba2d80d8be4612e8438447d38373e9b014b28172d49b7b6afeff462bc25bd4edc69c6e2c145dde052de54f47f4e36d74151fad3d08d21e3cad9563edc64adcca"); + expect(event.subscriptionId, + "181555e0fec2139d27ac80a5a46801415394e61d67be7f626631760dc5997bc0"); + }); }); } From 1b0dedf3c3ec6b948b2d81e1e22baced31e45bed Mon Sep 17 00:00:00 2001 From: ethicnology Date: Thu, 20 Apr 2023 09:05:51 +0200 Subject: [PATCH 05/16] refactor: PR #26 --- lib/src/crypto/kepler.dart | 38 +------- lib/src/crypto/operator.dart | 10 +-- lib/src/event.dart | 23 +---- lib/src/nips/nip_004/crypto.dart | 143 +++++++++++-------------------- lib/src/nips/nip_004/event.dart | 113 ++++++++++++++++-------- lib/src/utils.dart | 9 +- test/nips/nip_004_test.dart | 23 ++--- 7 files changed, 155 insertions(+), 204 deletions(-) diff --git a/lib/src/crypto/kepler.dart b/lib/src/crypto/kepler.dart index dc9d976..df1c042 100644 --- a/lib/src/crypto/kepler.dart +++ b/lib/src/crypto/kepler.dart @@ -1,12 +1,10 @@ +// credit: https://github.com/tjcampanella/kepler/blob/master/lib/src/kepler.dart + import 'package:convert/convert.dart'; import 'dart:typed_data'; import 'package:pointycastle/export.dart'; import 'operator.dart'; -/// -/// From archive repo: https://github.com/tjcampanella/kepler.git -/// - class Kepler { /// return a Bytes data secret static List> byteSecret(String privateString, String publicString) { @@ -17,11 +15,7 @@ class Kepler { final hexX = leftPadding(xs, 64); final hexY = leftPadding(ys, 64); final secretBytes = Uint8List.fromList(hex.decode('$hexX$hexY')); - final pair = [ - secretBytes.sublist(0, 32), - secretBytes.sublist(32, 40), - ]; - return pair; + return [secretBytes.sublist(0, 32), secretBytes.sublist(32, 40)]; } /// return a ECPoint data secret @@ -29,11 +23,7 @@ class Kepler { final privateKey = loadPrivateKey(privateString); final publicKey = loadPublicKey(publicString); assert(privateKey.d != null && publicKey.Q != null); - final secret = scalarMultiple( - privateKey.d!, - publicKey.Q!, - ); - return secret; + return scalarMultiple(privateKey.d!, publicKey.Q!); } static String leftPadding(String s, int width) { @@ -45,26 +35,6 @@ class Kepler { return "${paddingData.substring(0, paddingWidth)}$s"; } - static ECPoint scalarMultiple(BigInt k, ECPoint point) { - assert(isOnCurve(point)); - assert((k % theN).compareTo(BigInt.zero) != 0); - assert(point.x != null && point.y != null); - if (k < BigInt.from(0)) { - return scalarMultiple(-k, pointNeg(point)); - } - ECPoint? result; - ECPoint addend = point; - while (k > BigInt.from(0)) { - if (k & BigInt.from(1) > BigInt.from(0)) { - result = pointAdd(result, addend); - } - addend = pointAdd(addend, addend); - k >>= 1; - } - assert(isOnCurve(result!)); - return result!; - } - /// return a privateKey from hex string static ECPrivateKey loadPrivateKey(String storedkey) { final d = BigInt.parse(storedkey, radix: 16); diff --git a/lib/src/crypto/operator.dart b/lib/src/crypto/operator.dart index f16b690..231a66b 100644 --- a/lib/src/crypto/operator.dart +++ b/lib/src/crypto/operator.dart @@ -1,8 +1,6 @@ -import 'package:pointycastle/export.dart'; +// credit: https://github.com/tjcampanella/kepler/blob/master/lib/src/operator.dart -/// -/// From archive repo: https://github.com/tjcampanella/kepler.git -/// +import 'package:pointycastle/export.dart'; BigInt theP = BigInt.parse( "fffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f", @@ -80,10 +78,6 @@ ECPoint pointAdd(ECPoint? point1, ECPoint? point2) { final x2 = point2.x!.toBigInteger(); final y2 = point2.y!.toBigInteger(); - // assert(x1 != x2 && y1 == y2); - // if (x1 == x2 && y1 != y2) { - // return null; - // } BigInt m; if (x1 == x2) { m = (BigInt.from(3) * x1! * x1 + point1.curve.a!.toBigInteger()!) * 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..73385b4 100644 --- a/lib/src/nips/nip_004/crypto.dart +++ b/lib/src/nips/nip_004/crypto.dart @@ -1,105 +1,58 @@ import 'dart:convert'; -import 'dart:math'; import 'dart:typed_data'; +import 'package:nostr/nostr.dart'; +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], - ); - - // 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; +/// NIP4 cipher +String cipher( + String privkey, + String pubkey, + 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 = Uint8List.fromList(generateRandomBytes(16)); } - static String decipher( - 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; - } - 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 < input.length - 16) { + offset += algo.processBlock(input, offset, output, offset); + } + offset += algo.doFinal(input, offset, output, offset); + Uint8List result = output.sublist(0, offset); - var offset = 0; - while (offset < cipherText.length - 16) { - offset += - cipherImpl.processBlock(cipherText, offset, finalPlainText, offset); - } - //remove padding - offset += cipherImpl.doFinal(cipherText, offset, finalPlainText, offset); - Uint8List result = finalPlainText.sublist(0, offset); + if (cipher) { + String stringIv = base64.encode(iv); + String plaintext = base64.encode(result); + return "$plaintext?iv=$stringIv"; + } else { return Utf8Decoder().convert(result); } } diff --git a/lib/src/nips/nip_004/event.dart b/lib/src/nips/nip_004/event.dart index f437f03..c76a951 100644 --- a/lib/src/nips/nip_004/event.dart +++ b/lib/src/nips/nip_004/event.dart @@ -3,10 +3,20 @@ import 'package:nostr/src/event.dart'; import 'package:nostr/src/nips/nip_004/crypto.dart'; import 'package:nostr/src/utils.dart'; +/// A special event with kind 4, meaning "encrypted direct message". +/// +/// content MUST be equal to the base64-encoded, aes-256-cbc encrypted string of anything a user wants to write, encrypted using a shared cipher generated by combining the recipient's public-key with the sender's private-key; +/// this appended by the base64-encoded initialization vector as if it was a querystring parameter named "iv". +/// The format is the following: "content": "?iv=". +/// +/// tags MUST contain an entry identifying the receiver of the message (such that relays may naturally forward this event to them), in the form ["p", "pubkey, as a hex string"]. +/// +/// tags MAY contain an entry identifying the previous message in a conversation or a message we are explicitly replying to (such that contextual, more organized conversations may happen), in the form ["e", "event_id"]. +/// +/// Note: By default in the libsecp256k1 ECDH implementation, the secret is the SHA256 hash of the shared point (both X and Y coordinates). In Nostr, only the X coordinate of the shared point is used as the secret and it is NOT hashed. If using libsecp256k1, a custom function that copies the X coordinate must be passed as the hashfp argument in secp256k1_ecdh. class EncryptedDirectMessage extends Event { - static Map>> gMapByteSecret = {}; - - EncryptedDirectMessage(Event event) + /// Default constructor + EncryptedDirectMessage(Event event, {verify = true}) : super( event.id, event.pubkey, @@ -16,60 +26,93 @@ class EncryptedDirectMessage extends Event { event.content, event.sig, subscriptionId: event.subscriptionId, - verify: true, + verify: verify, ); - factory EncryptedDirectMessage.quick( + /// receive an EncryptedDirectMessage + EncryptedDirectMessage.receive(Event event, {verify = true}) + : super( + event.id, + event.pubkey, + event.createdAt, + event.kind, + event.tags, + event.content, + event.sig, + subscriptionId: event.subscriptionId, + verify: verify, + ) { + assert(kind == 4); + } + + /// prepare a EncryptedDirectMessage to send quickly with the minimal needed params + factory EncryptedDirectMessage.redact( String senderPrivkey, String receiverPubkey, String message, ) { - var event = Event.partial(); - event.pubkey = bip340.getPublicKey(senderPrivkey).toLowerCase(); - event.createdAt = currentUnixTimestampSeconds(); - event.kind = 4; - event.tags = [ - ['p', receiverPubkey] - ]; - event.content = Nip4.cipher(senderPrivkey, '02$receiverPubkey', message); + var event = Event.partial( + pubkey: bip340.getPublicKey(senderPrivkey).toLowerCase(), + createdAt: currentUnixTimestampSeconds(), + kind: 4, + tags: [ + ['p', receiverPubkey] + ], + content: cipher( + senderPrivkey, + '02$receiverPubkey', + message, + true, + ), + ); event.id = event.getEventId(); event.sig = event.getSignature(senderPrivkey); return EncryptedDirectMessage(event); } - String? get receiverPubkey => findPubkey(); + /// get receiver public key + String? get receiver => _findTag("p"); - String getCiphertext(String senderPrivkey, String receiverPubkey) { - String ciphertext = - Nip4.cipher(senderPrivkey, '02$receiverPubkey', content); - return ciphertext; - } + /// get sender public key + String? get sender => pubkey; - String getPlaintext(String receiverPrivkey, [String senderPubkey=""]) { - if (senderPubkey.length == 0) { - senderPubkey = pubkey; - } - 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); + /// get previous event id –> MAY contain an entry identifying the previous message in a conversation or a message we are explicitly replying to. + String? get previous => _findTag("e"); + + /// get nonce/IV + String get nonce => _findNonce(); + + /// get the deciphered message a.k.a. plaintext + String getPlaintext(String privkey) { + String ciphertext = content.split("?iv=")[0]; + String plaintext; try { - plaintext = Nip4.decipher(receiverPrivkey, "02$senderPubkey", ciphertext, iv); - } catch(e) { - print("Fail to decrypt: ${e}"); + plaintext = cipher( + privkey, + "02$pubkey", + ciphertext, + false, + nonce: nonce, + ); + } catch (e) { + throw Exception("Fail to decipher: $e"); } return plaintext; } - String? findPubkey() { + /// find the given tag prefix and return the value if found + String? _findTag(String prefix) { String prefix = "p"; for (List tag in tags) { if (tag.isNotEmpty && tag[0] == prefix && tag.length > 1) return tag[1]; } return null; } + + /// parse the ciphered content to return the nonce/IV + String _findNonce() { + List split = content.split("?iv="); + if (split.length != 2) throw Exception("invalid content or non ciphered"); + return split[1]; + } } diff --git a/lib/src/utils.dart b/lib/src/utils.dart index b4269a6..37fd8e0 100644 --- a/lib/src/utils.dart +++ b/lib/src/utils.dart @@ -4,8 +4,7 @@ import 'package:convert/convert.dart'; /// generates 32 random bytes converted in hex String generate64RandomHexChars() { - final random = Random.secure(); - final randomBytes = List.generate(32, (i) => random.nextInt(256)); + final randomBytes = generateRandomBytes(32); return hex.encode(randomBytes); } @@ -13,3 +12,9 @@ String generate64RandomHexChars() { int currentUnixTimestampSeconds() { return DateTime.now().millisecondsSinceEpoch ~/ 1000; } + +/// generates the requested quantity of random secure bytes +List generateRandomBytes(int quantity) { + final random = Random.secure(); + return List.generate(quantity, (i) => random.nextInt(256)); +} diff --git a/test/nips/nip_004_test.dart b/test/nips/nip_004_test.dart index f10f075..720a005 100644 --- a/test/nips/nip_004_test.dart +++ b/test/nips/nip_004_test.dart @@ -2,13 +2,13 @@ import 'package:nostr/nostr.dart'; import 'package:test/test.dart'; String bobPubkey = - "2d38a56c4303bc722370c50c86fc8dd3327f06a8fe59b3ff3d670738d71dd1e1"; + "2d38a56c4303bc722370c50c86fc8dd3327f06a8fe59b3ff3d670738d71dd1e1"; String alicePubkey = - "0f76c800a7ea76b83a3ae87de94c6046b98311bda8885cedd8420885b50de181"; + "0f76c800a7ea76b83a3ae87de94c6046b98311bda8885cedd8420885b50de181"; String bobPrivkey = - "826ef0e93c1278bd89945377fadb6b6b51d9eedf74ecdb64a96f1897bb670be8"; + "826ef0e93c1278bd89945377fadb6b6b51d9eedf74ecdb64a96f1897bb670be8"; String alicePrivkey = - "773dc29ff81f7680eeca5d530f528e8c572979b46abc8bfd1586b73a6a98ab4d"; + "773dc29ff81f7680eeca5d530f528e8c572979b46abc8bfd1586b73a6a98ab4d"; void main() { group('EncryptedDirectMessage', () { @@ -20,9 +20,9 @@ void main() { ]; EncryptedDirectMessage event = - EncryptedDirectMessage.quick(bobPrivkey, alicePubkey, plaintext); + EncryptedDirectMessage.redact(bobPrivkey, alicePubkey, plaintext); - expect(event.receiverPubkey, alicePubkey); + expect(event.receiver, alicePubkey); expect(event.getPlaintext(alicePrivkey), plaintext); expect(event.pubkey, bobPubkey); expect(event.kind, 4); @@ -32,24 +32,25 @@ void main() { test('EncryptedDirectMessage Receive', () { String receivedEvent = - '["EVENT", "181555e0fec2139d27ac80a5a46801415394e61d67be7f626631760dc5997bc0", {"id": "2739cccdc3fa943ad447378e234ef1325a76f023a169b483b6fe8cab47a793e1", "pubkey": "0f76c800a7ea76b83a3ae87de94c6046b98311bda8885cedd8420885b50de181", "created_at": 1680475069, "kind": 4, "tags": [["p", "2d38a56c4303bc722370c50c86fc8dd3327f06a8fe59b3ff3d670738d71dd1e1"]], "content": "hH1HlQWY3dz7IzJlgnEgW1WNtA0KlvGgo/OC4mep/R4I6PMqJuvZ35j4OFHkMvqb?iv=jbkXPH2esn5DIP3BodpsTQ==", "sig": "ba2d80d8be4612e8438447d38373e9b014b28172d49b7b6afeff462bc25bd4edc69c6e2c145dde052de54f47f4e36d74151fad3d08d21e3cad9563edc64adcca"}]'; + '["EVENT", "181555e0fec2139d27ac80a5a46801415394e61d67be7f626631760dc5997bc0", {"id": "2739cccdc3fa943ad447378e234ef1325a76f023a169b483b6fe8cab47a793e1", "pubkey": "0f76c800a7ea76b83a3ae87de94c6046b98311bda8885cedd8420885b50de181", "created_at": 1680475069, "kind": 4, "tags": [["p", "2d38a56c4303bc722370c50c86fc8dd3327f06a8fe59b3ff3d670738d71dd1e1"]], "content": "hH1HlQWY3dz7IzJlgnEgW1WNtA0KlvGgo/OC4mep/R4I6PMqJuvZ35j4OFHkMvqb?iv=jbkXPH2esn5DIP3BodpsTQ==", "sig": "ba2d80d8be4612e8438447d38373e9b014b28172d49b7b6afeff462bc25bd4edc69c6e2c145dde052de54f47f4e36d74151fad3d08d21e3cad9563edc64adcca"}]'; Message m = Message.deserialize(receivedEvent); Event event = m.message; expect(event.id, - "2739cccdc3fa943ad447378e234ef1325a76f023a169b483b6fe8cab47a793e1"); + "2739cccdc3fa943ad447378e234ef1325a76f023a169b483b6fe8cab47a793e1"); expect(event.pubkey, alicePubkey); expect(event.createdAt, 1680475069); expect(event.kind, 4); expect(event.tags, [ ["p", bobPubkey] ]); - String content = (event as EncryptedDirectMessage).getPlaintext(bobPrivkey); + String content = + (event as EncryptedDirectMessage).getPlaintext(bobPrivkey); expect(content, "Secret message from alice to bob!"); expect(event.sig, - "ba2d80d8be4612e8438447d38373e9b014b28172d49b7b6afeff462bc25bd4edc69c6e2c145dde052de54f47f4e36d74151fad3d08d21e3cad9563edc64adcca"); + "ba2d80d8be4612e8438447d38373e9b014b28172d49b7b6afeff462bc25bd4edc69c6e2c145dde052de54f47f4e36d74151fad3d08d21e3cad9563edc64adcca"); expect(event.subscriptionId, - "181555e0fec2139d27ac80a5a46801415394e61d67be7f626631760dc5997bc0"); + "181555e0fec2139d27ac80a5a46801415394e61d67be7f626631760dc5997bc0"); }); }); } From 9b52f8c33853925453411d30fcd9fc7620b863c1 Mon Sep 17 00:00:00 2001 From: ethicnology Date: Thu, 20 Apr 2023 13:46:44 +0200 Subject: [PATCH 06/16] chore: rename/reorganize files from #25 & #26 --- lib/nostr.dart | 2 +- lib/src/{nips/nip_004/crypto.dart => crypto/nip_004.dart} | 2 +- lib/src/nips/{nip_004/event.dart => nip_004.dart} | 8 ++++---- 3 files changed, 6 insertions(+), 6 deletions(-) rename lib/src/{nips/nip_004/crypto.dart => crypto/nip_004.dart} (98%) rename lib/src/nips/{nip_004/event.dart => nip_004.dart} (95%) diff --git a/lib/nostr.dart b/lib/nostr.dart index 5bc4d95..2176da7 100644 --- a/lib/nostr.dart +++ b/lib/nostr.dart @@ -11,4 +11,4 @@ export 'src/close.dart'; export 'src/message.dart'; export 'src/utils.dart'; export 'src/nips/nip_002.dart'; -export 'src/nips/nip_004/event.dart'; +export 'src/nips/nip_004.dart'; diff --git a/lib/src/nips/nip_004/crypto.dart b/lib/src/crypto/nip_004.dart similarity index 98% rename from lib/src/nips/nip_004/crypto.dart rename to lib/src/crypto/nip_004.dart index 73385b4..6e49ae7 100644 --- a/lib/src/nips/nip_004/crypto.dart +++ b/lib/src/crypto/nip_004.dart @@ -5,7 +5,7 @@ import 'package:nostr/src/crypto/kepler.dart'; import 'package:pointycastle/export.dart'; /// NIP4 cipher -String cipher( +String nip4cipher( String privkey, String pubkey, String payload, diff --git a/lib/src/nips/nip_004/event.dart b/lib/src/nips/nip_004.dart similarity index 95% rename from lib/src/nips/nip_004/event.dart rename to lib/src/nips/nip_004.dart index c76a951..aec1fc4 100644 --- a/lib/src/nips/nip_004/event.dart +++ b/lib/src/nips/nip_004.dart @@ -1,11 +1,11 @@ import 'package:bip340/bip340.dart' as bip340; import 'package:nostr/src/event.dart'; -import 'package:nostr/src/nips/nip_004/crypto.dart'; +import 'package:nostr/src/crypto/nip_004.dart'; import 'package:nostr/src/utils.dart'; /// A special event with kind 4, meaning "encrypted direct message". /// -/// content MUST be equal to the base64-encoded, aes-256-cbc encrypted string of anything a user wants to write, encrypted using a shared cipher generated by combining the recipient's public-key with the sender's private-key; +/// content MUST be equal to the base64-encoded, aes-256-cbc encrypted string of anything a user wants to write, encrypted using a shared nip4cipher generated by combining the recipient's public-key with the sender's private-key; /// this appended by the base64-encoded initialization vector as if it was a querystring parameter named "iv". /// The format is the following: "content": "?iv=". /// @@ -58,7 +58,7 @@ class EncryptedDirectMessage extends Event { tags: [ ['p', receiverPubkey] ], - content: cipher( + content: nip4cipher( senderPrivkey, '02$receiverPubkey', message, @@ -87,7 +87,7 @@ class EncryptedDirectMessage extends Event { String ciphertext = content.split("?iv=")[0]; String plaintext; try { - plaintext = cipher( + plaintext = nip4cipher( privkey, "02$pubkey", ciphertext, From bd664bf02ca5c7bacff1e2e3f68284e8eab01dd7 Mon Sep 17 00:00:00 2001 From: water <130329555+water783@users.noreply.github.com> Date: Wed, 24 May 2023 20:56:24 +0800 Subject: [PATCH 07/16] feat: NIPS 1, 5, 10, 19, 20, 28, 51 --- lib/nostr.dart | 8 ++ lib/src/nips/nip_001.dart | 19 +++ lib/src/nips/nip_005.dart | 78 ++++++++++++ lib/src/nips/nip_010.dart | 70 +++++++++++ lib/src/nips/nip_019.dart | 91 ++++++++++++++ lib/src/nips/nip_020.dart | 20 +++ lib/src/nips/nip_028.dart | 238 ++++++++++++++++++++++++++++++++++++ lib/src/nips/nip_051.dart | 173 ++++++++++++++++++++++++++ pubspec.yaml | 2 + test/nips/nip_005_test.dart | 41 +++++++ test/nips/nip_010_test.dart | 36 ++++++ test/nips/nip_019_test.dart | 38 ++++++ test/nips/nip_020_test.dart | 14 +++ test/nips/nip_028_test.dart | 69 +++++++++++ test/nips/nip_051_test.dart | 73 +++++++++++ 15 files changed, 970 insertions(+) create mode 100644 lib/src/nips/nip_001.dart create mode 100644 lib/src/nips/nip_005.dart create mode 100644 lib/src/nips/nip_010.dart create mode 100644 lib/src/nips/nip_019.dart create mode 100644 lib/src/nips/nip_020.dart create mode 100644 lib/src/nips/nip_028.dart create mode 100644 lib/src/nips/nip_051.dart create mode 100644 test/nips/nip_005_test.dart create mode 100644 test/nips/nip_010_test.dart create mode 100644 test/nips/nip_019_test.dart create mode 100644 test/nips/nip_020_test.dart create mode 100644 test/nips/nip_028_test.dart create mode 100644 test/nips/nip_051_test.dart diff --git a/lib/nostr.dart b/lib/nostr.dart index 2176da7..f91d30f 100644 --- a/lib/nostr.dart +++ b/lib/nostr.dart @@ -10,5 +10,13 @@ export 'src/filter.dart'; export 'src/close.dart'; export 'src/message.dart'; export 'src/utils.dart'; +export 'src/nips/nip_001.dart'; export 'src/nips/nip_002.dart'; export 'src/nips/nip_004.dart'; +export 'src/nips/nip_005.dart'; +export 'src/nips/nip_010.dart'; +export 'src/nips/nip_019.dart'; +export 'src/nips/nip_020.dart'; +export 'src/nips/nip_028.dart'; +export 'src/nips/nip_051.dart'; + diff --git a/lib/src/nips/nip_001.dart b/lib/src/nips/nip_001.dart new file mode 100644 index 0000000..5f0ec44 --- /dev/null +++ b/lib/src/nips/nip_001.dart @@ -0,0 +1,19 @@ +import 'package:nostr/nostr.dart'; + +/// Basic Event Kinds +/// 0: set_metadata: the content is set to a stringified JSON object {name: , about: , picture: } describing the user who created the event. A relay may delete past set_metadata events once it gets a new one for the same pubkey. +/// 1: text_note: the content is set to the plaintext content of a note (anything the user wants to say). Do not use Markdown! Clients should not have to guess how to interpret content like [](). Use different event kinds for parsable content. +/// 2: recommend_server: the content is set to the URL (e.g., wss://somerelay.com) of a relay the event creator wants to recommend to its followers. +class Nip1 { + static Event setMetadata(String content, String privkey) { + return Event.from(kind: 0, tags: [], content: content, privkey: privkey); + } + + static Event textNote(String content, String privkey) { + return Event.from(kind: 1, tags: [], content: content, privkey: privkey); + } + + static Event recommendServer(String content, String privkey) { + return Event.from(kind: 2, tags: [], content: content, privkey: privkey); + } +} diff --git a/lib/src/nips/nip_005.dart b/lib/src/nips/nip_005.dart new file mode 100644 index 0000000..9a9e578 --- /dev/null +++ b/lib/src/nips/nip_005.dart @@ -0,0 +1,78 @@ +import 'dart:convert'; +import 'package:nostr/nostr.dart'; + +/// Mapping Nostr keys to DNS-based internet identifiers +class Nip5 { + /// decode setmetadata event + /// { + /// "pubkey": "b0635d6a9851d3aed0cd6c495b282167acf761729078d975fc341b22650b07b9", + /// "kind": 0, + /// "content": "{\"name\": \"bob\", \"nip05\": \"bob@example.com\"}" + /// } + static Future decode(Event event) async { + if (event.kind == 0) { + try { + Map map = jsonDecode(event.content); + String dns = map['nip05']; + List relays = map['relays']; + if (dns.isNotEmpty) { + List parts = dns.split('@'); + String name = parts[0]; + String domain = parts[1]; + return DNS(name, domain, event.pubkey, relays.map((e) => e.toString()).toList()); + } + } catch (e) { + throw Exception(e.toString()); + } + } + throw Exception("${event.kind} is not nip1 compatible"); + } + + /// encode set metadata event + static Event encode( + String name, String domain, List relays, String privkey) { + if (isValidName(name) && isValidDomain(domain)) { + String content = generateContent(name, domain, relays); + return Nip1.setMetadata(content, privkey); + } else { + throw Exception("not a valid name or domain!"); + } + } + + static bool isValidName(String input) { + RegExp regExp = RegExp(r'^[a-z0-9_]+$'); + return regExp.hasMatch(input); + } + + static bool isValidDomain(String domain) { + RegExp regExp = RegExp( + r'^([a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{2,}$', + caseSensitive: false, + ); + return regExp.hasMatch(domain); + } + + static String generateContent( + String name, String domain, List relays) { + Map map = { + 'name': name, + 'nip05': '$name@$domain', + 'relays': relays, + }; + return jsonEncode(map); + } +} + +/// +class DNS { + String name; + + String domain; + + String pubkey; + + List relays; + + /// Default constructor + DNS(this.name, this.domain, this.pubkey, this.relays); +} diff --git a/lib/src/nips/nip_010.dart b/lib/src/nips/nip_010.dart new file mode 100644 index 0000000..f712f0c --- /dev/null +++ b/lib/src/nips/nip_010.dart @@ -0,0 +1,70 @@ +///This NIP describes how to use "e" and "p" tags in text events, +///especially those that are replies to other text events. +///It helps clients thread the replies into a tree rooted at the original event. + +class Nip10 { + ///{ + /// "tags": [ + /// ["e", , , "root"], + /// ["e", , , "reply"], + /// ["p", , ], + /// ... + /// ], + /// ... + /// } + static Thread fromTags(List> tags) { + ETag root = ETag('', '', ''); + List etags = []; + List ptags = []; + for (var tag in tags) { + if (tag[0] == "p") ptags.add(PTag(tag[1], tag[2])); + if (tag[0] == "e") { + if (tag[3] == 'root') { + root = ETag(tag[1], tag[2], tag[3]); + } else { + etags.add(ETag(tag[1], tag[2], tag[3])); + } + } + } + return Thread(root, etags, ptags); + } + + static ETag rootTag(String eventId, String relay) { + return ETag(eventId, relay, 'root'); + } + + static List> toTags(Thread thread) { + List> result = []; + result.add( + ["e", thread.root.eventId, thread.root.relayURL, thread.root.marker]); + for (var etag in thread.etags) { + result.add(["e", etag.eventId, etag.relayURL, etag.marker]); + } + for (var ptag in thread.ptags) { + result.add(["p", ptag.pubkey, ptag.relayURL]); + } + return result; + } +} + +class ETag { + String eventId; + String relayURL; + String marker; // root, reply, mention + + ETag(this.eventId, this.relayURL, this.marker); +} + +class PTag { + String pubkey; + String relayURL; + + PTag(this.pubkey, this.relayURL); +} + +class Thread { + ETag root; + List etags; + List ptags; + Thread(this.root, this.etags, this.ptags); +} diff --git a/lib/src/nips/nip_019.dart b/lib/src/nips/nip_019.dart new file mode 100644 index 0000000..d70bf32 --- /dev/null +++ b/lib/src/nips/nip_019.dart @@ -0,0 +1,91 @@ +import 'package:bech32/bech32.dart'; +import 'package:convert/convert.dart'; + +/// bech32-encoded entities +class Nip19 { + static encodePubkey(String pubkey) { + return bech32Encode("npub", pubkey); + } + + static encodePrivkey(String privkey) { + return bech32Encode("nsec", privkey); + } + + static encodeNote(String noteid) { + return bech32Encode("note", noteid); + } + + static String decodePubkey(String data) { + Map map = bech32Decode(data); + if (map["prefix"] == "npub") { + return map["data"]; + } else { + return ""; + } + } + + static String decodePrivkey(String data) { + Map map = bech32Decode(data); + if (map["prefix"] == "nsec") { + return map["data"]; + } else { + return ""; + } + } + + static String decodeNote(String data) { + Map map = bech32Decode(data); + if (map["prefix"] == "note") { + return map["data"]; + } else { + return ""; + } + } +} + +/// help functions + +String bech32Encode(String prefix, String hexData) { + final data = hex.decode(hexData); + final convertedData = convertBits(data, 8, 5, true); + final bech32Data = Bech32(prefix, convertedData); + return bech32.encode(bech32Data); +} + +Map bech32Decode(String bech32Data) { + final decodedData = bech32.decode(bech32Data); + final convertedData = convertBits(decodedData.data, 5, 8, false); + final hexData = hex.encode(convertedData); + + return {'prefix': decodedData.hrp, 'data': hexData}; +} + +List convertBits(List data, int fromBits, int toBits, bool pad) { + var acc = 0; + var bits = 0; + final maxv = (1 << toBits) - 1; + final result = []; + + for (final value in data) { + if (value < 0 || value >> fromBits != 0) { + throw Exception('Invalid value: $value'); + } + acc = (acc << fromBits) | value; + bits += fromBits; + + while (bits >= toBits) { + bits -= toBits; + result.add((acc >> bits) & maxv); + } + } + + if (pad) { + if (bits > 0) { + result.add((acc << (toBits - bits)) & maxv); + } + } else if (bits >= fromBits || ((acc << (toBits - bits)) & maxv) != 0) { + throw Exception('Invalid data'); + } + + return result; +} diff --git a/lib/src/nips/nip_020.dart b/lib/src/nips/nip_020.dart new file mode 100644 index 0000000..cf75f0f --- /dev/null +++ b/lib/src/nips/nip_020.dart @@ -0,0 +1,20 @@ +import 'dart:convert'; +import 'package:nostr/nostr.dart'; + +class Nip20 { + static OKEvent? getOk(String okPayload) { + var ok = Message.deserialize(okPayload); + if(ok.type == 'OK'){ + var object = jsonDecode(ok.message); + return OKEvent(object[0], object[1], object[2]); + } + } +} + +class OKEvent { + String eventId; + bool status; + String message; + + OKEvent(this.eventId, this.status, this.message); +} \ No newline at end of file diff --git a/lib/src/nips/nip_028.dart b/lib/src/nips/nip_028.dart new file mode 100644 index 0000000..d93ebd3 --- /dev/null +++ b/lib/src/nips/nip_028.dart @@ -0,0 +1,238 @@ +import 'dart:convert'; +import 'package:nostr/nostr.dart'; + +/// Public Chat & Channel +class Nip28 { + static Channel getChannelCreation(Event event) { + try { + Map content = jsonDecode(event.content); + if (event.kind == 40) { + // create channel + Map additional = Map.from(content); + String? name = additional.remove("name"); + String? about = additional.remove("about"); + String? picture = additional.remove("picture"); + return Channel( + event.id, name!, about!, picture!, event.pubkey, additional); + } else { + throw Exception("${event.kind} is not nip28 compatible"); + } + } catch (e) { + throw Exception(e.toString()); + } + } + + static Channel getChannelMetadata(Event event) { + try { + Map content = jsonDecode(event.content); + if (event.kind == 41) { + // create channel + Map additional = Map.from(content); + String? name = additional.remove("name"); + String? about = additional.remove("about"); + String? picture = additional.remove("picture"); + String? channelId; + String? relay; + for (var tag in event.tags) { + if (tag[0] == "e") { + channelId = tag[1]; + relay = tag[2]; + } + } + Channel result = Channel( + channelId!, name!, about!, picture!, event.pubkey, additional); + result.relay = relay; + return result; + } else { + throw Exception("${event.kind} is not nip28 compatible"); + } + } catch (e) { + throw Exception(e.toString()); + } + } + + static ChannelMessage getChannelMessage(Event event) { + try { + if (event.kind == 42) { + var content = event.content; + Thread thread = Nip10.fromTags(event.tags); + String channelId = thread.root.eventId; + return ChannelMessage( + channelId, event.pubkey, content, thread, event.createdAt); + } + throw Exception("${event.kind} is not nip28 compatible"); + } catch (e) { + throw Exception(e.toString()); + } + } + + static ChannelMessageHidden getMessageHidden(Event event) { + try { + if (event.kind == 43) { + String? messageId; + for (var tag in event.tags) { + if (tag[0] == "e") { + messageId = tag[1]; + break; + } + } + Map content = jsonDecode(event.content); + String reason = content['reason']; + return ChannelMessageHidden( + event.pubkey, messageId!, reason, event.createdAt); + } + throw Exception("${event.kind} is not nip28(hide message) compatible"); + } catch (e) { + throw Exception(e.toString()); + } + } + + static ChannelUserMuted getUserMuted(Event event) { + try { + if (event.kind == 44) { + String? userPubkey; + for (var tag in event.tags) { + if (tag[0] == "p") { + userPubkey = tag[1]; + break; + } + } + Map content = jsonDecode(event.content); + String reason = content['reason']; + return ChannelUserMuted( + event.pubkey, userPubkey!, reason, event.createdAt); + } + throw Exception("${event.kind} is not nip28(mute user) compatible"); + } catch (e) { + throw Exception(e.toString()); + } + } + + static Event createChannel(String name, String about, String picture, + Map additional, String privkey) { + Map map = { + 'name': name, + 'about': about, + 'picture': picture, + }; + map.addAll(additional); + String content = jsonEncode(map); + Event event = + Event.from(kind: 40, tags: [], content: content, privkey: privkey); + return event; + } + + static Event setChannelMetaData( + String name, + String about, + String picture, + Map additional, + String channelId, + String relayURL, + String privkey) { + Map map = { + 'name': name, + 'about': about, + 'picture': picture, + }; + map.addAll(additional); + String content = jsonEncode(map); + List> tags = []; + tags.add(["e", channelId, relayURL]); + Event event = + Event.from(kind: 41, tags: tags, content: content, privkey: privkey); + return event; + } + + static Event sendChannelMessage( + String channelId, String content, String privkey, + {String? relay, List? etags, List? ptags}) { + List> tags = []; + Thread t = + Thread(Nip10.rootTag(channelId, relay ?? ''), etags ?? [], ptags ?? []); + tags = Nip10.toTags(t); + Event event = + Event.from(kind: 42, tags: tags, content: content, privkey: privkey); + return event; + } + + static Event hideChannelMessage( + String messageId, String reason, String privkey) { + Map map = { + 'reason': reason, + }; + String content = jsonEncode(map); + List> tags = []; + tags.add(["e", messageId]); + Event event = + Event.from(kind: 43, tags: tags, content: content, privkey: privkey); + return event; + } + + static Event muteUser(String pubkey, String reason, String privkey) { + Map map = { + 'reason': reason, + }; + String content = jsonEncode(map); + List> tags = []; + tags.add(["p", pubkey]); + Event event = + Event.from(kind: 44, tags: tags, content: content, privkey: privkey); + return event; + } +} + +/// channel info +class Channel { + /// channel create event id + String channelId; + + String name; + + String about; + + String picture; + + String owner; + + String? relay; + + /// Clients MAY add additional metadata fields. + Map additional; + + /// Default constructor + Channel(this.channelId, this.name, this.about, this.picture, this.owner, + this.additional); +} + +/// messages in channel +class ChannelMessage { + String channelId; + String sender; + String content; + Thread thread; + int createTime; + + ChannelMessage( + this.channelId, this.sender, this.content, this.thread, this.createTime); +} + +class ChannelMessageHidden { + String operator; + String messageId; + String reason; + int createTime; + + ChannelMessageHidden( + this.operator, this.messageId, this.reason, this.createTime); +} + +class ChannelUserMuted { + String operator; + String userPubkey; + String reason; + int createTime; + + ChannelUserMuted( + this.operator, this.userPubkey, this.reason, this.createTime); +} diff --git a/lib/src/nips/nip_051.dart b/lib/src/nips/nip_051.dart new file mode 100644 index 0000000..f0483a3 --- /dev/null +++ b/lib/src/nips/nip_051.dart @@ -0,0 +1,173 @@ +import 'dart:convert'; +import 'package:bip340/bip340.dart' as bip340; +import 'package:nostr/nostr.dart'; +import '../crypto/nip_004.dart'; + +/// Lists +class Nip51 { + static List> peoplesToTags(List items) { + List> result = []; + for (People item in items) { + result.add([ + "p", + item.pubkey, + item.mainRelay ?? "", + item.petName ?? "", + item.aliasPubKey ?? "", + ]); + } + return result; + } + + static List> bookmarksToTags(List items) { + List> result = []; + for (String item in items) { + result.add(["e", item]); + } + return result; + } + + static String peoplesToContent( + List items, String privkey, String pubkey) { + var list = []; + for (People item in items) { + list.add([ + 'p', + item.pubkey, + item.mainRelay ?? "", + item.petName ?? "", + item.aliasPubKey ?? "", + ]); + } + String content = jsonEncode(list); + return nip4cipher(privkey, '02$pubkey', content, true); + } + + static String bookmarksToContent( + List items, String privkey, String pubkey) { + var list = []; + for (String item in items) { + list.add(['e', item]); + } + String content = jsonEncode(list); + return nip4cipher(privkey, '02$pubkey', content, true); + } + + static Map fromContent( + String content, String privkey, String pubkey) { + List people = []; + List bookmarks = []; + int ivIndex = content.indexOf("?iv="); + if (ivIndex <= 0) { + print("Invalid content, could not get ivIndex: $content"); + } + String iv = content.substring(ivIndex + "?iv=".length, content.length); + String encString = content.substring(0, ivIndex); + String deContent = + nip4cipher(privkey, "02$pubkey", encString, false, nonce: iv); + for (List tag in jsonDecode(deContent)) { + if (tag[0] == "p") { + people.add(People(tag[1], tag.length > 2 ? tag[2] : "", + tag.length > 3 ? tag[3] : "", tag.length > 4 ? tag[4] : "")); + } else if (tag[0] == "e") { + // bookmark + bookmarks.add(tag[1]); + } + } + return {"people": people, "bookmarks": bookmarks}; + } + + static Event createMutePeople(List items, List encryptedItems, + String privkey, String pubkey) { + return Event.from( + kind: 10000, + tags: peoplesToTags(items), + content: peoplesToContent(encryptedItems, privkey, pubkey), + privkey: privkey); + } + + static createPinEvent(List items, List encryptedItems, + String privkey, String pubkey) { + return Event.from( + kind: 10001, + tags: bookmarksToTags(items), + content: bookmarksToContent(encryptedItems, privkey, pubkey), + privkey: privkey); + } + + static Event createCategorizedPeople(String identifier, List items, + List encryptedItems, String privkey, String pubkey) { + List> tags = peoplesToTags(items); + tags.add(["d", identifier]); + return Event.from( + kind: 30000, + tags: tags, + content: peoplesToContent(encryptedItems, privkey, pubkey), + privkey: privkey); + } + + static createCategorizedBookmarks(String identifier, List items, + List encryptedItems, String privkey, String pubkey) { + List> tags = bookmarksToTags(items); + tags.add(["d", identifier]); + return Event.from( + kind: 30001, + tags: tags, + content: bookmarksToContent(encryptedItems, privkey, pubkey), + privkey: privkey); + } + + static Lists getLists(Event event, String privkey) { + if (event.kind != 10000 && + event.kind != 10001 && + event.kind != 30000 && + event.kind != 30001) { + throw Exception("${event.kind} is not nip51 compatible"); + } + String identifier = ""; + List people = []; + List bookmarks = []; + for (List tag in event.tags) { + if (tag[0] == "p") { + people.add(People(tag[1], tag.length > 2 ? tag[2] : "", + tag.length > 3 ? tag[3] : "", tag.length > 4 ? tag[4] : "")); + } + if (tag[0] == "e") { + bookmarks.add(tag[1]); + } + if (tag[0] == "d") identifier = tag[1]; + } + String pubkey = bip340.getPublicKey(privkey); + Map content = Nip51.fromContent(event.content, privkey, pubkey); + people.addAll(content["people"]); + bookmarks.addAll(content["bookmarks"]); + if (event.kind == 10000) identifier = "Mute"; + if (event.kind == 10001) identifier = "Pin"; + + return Lists(event.pubkey, identifier, people, bookmarks); + } +} + +/// +class People { + String pubkey; + String? mainRelay; + String? petName; + String? aliasPubKey; + + /// Default constructor + People(this.pubkey, this.mainRelay, this.petName, this.aliasPubKey); +} + +class Lists { + String owner; + + String identifier; + + List people; + + List bookmarks; + + /// Default constructor + Lists(this.owner, this.identifier, this.people, this.bookmarks); +} diff --git a/pubspec.yaml b/pubspec.yaml index 088a90d..0f9efea 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -13,4 +13,6 @@ dependencies: bip340: ^0.1.0 convert: ^3.1.1 pointycastle: ^3.7.3 + bech32: ^0.2.2 + \ No newline at end of file diff --git a/test/nips/nip_005_test.dart b/test/nips/nip_005_test.dart new file mode 100644 index 0000000..7fab14a --- /dev/null +++ b/test/nips/nip_005_test.dart @@ -0,0 +1,41 @@ +import 'package:nostr/nostr.dart'; +import 'package:test/test.dart'; + +void main() { + group('nip005', () { + test('encode', () { + var hex = + "5ee1c8000ab28edd64d74a7d951ac2dd559814887b1b9e1ac7c5f89e96125c12"; + var user = Keychain(hex); + List relays = [ + 'wss://relay.example.com', + 'wss://relay2.example.com' + ]; + Event event = Nip5.encode('name', 'example.com', relays, user.private); + print(event.serialize()); + expect(event.kind, 0); + + expect(() => Nip5.encode('name', 'example', relays, user.private), + throwsException); + expect(() => Nip5.encode('name!', 'example.com', relays, user.private), + throwsException); + }); + + test('decode', () async { + var event = Event.from( + kind: 0, + tags: [], + content: + "{\"name\":\"name\",\"nip05\":\"name@example.com\",\"relays\":[\"wss://relay.example.com\",\"wss://relay2.example.com\"]}", + privkey: + "5ee1c8000ab28edd64d74a7d951ac2dd559814887b1b9e1ac7c5f89e96125c12", + ); + DNS? dns = await Nip5.decode(event); + expect(dns!.name, 'name'); + expect(dns.domain, 'example.com'); + expect(dns.pubkey, event.pubkey); + expect( + dns.relays, ['wss://relay.example.com', 'wss://relay2.example.com']); + }); + }); +} diff --git a/test/nips/nip_010_test.dart b/test/nips/nip_010_test.dart new file mode 100644 index 0000000..79e2105 --- /dev/null +++ b/test/nips/nip_010_test.dart @@ -0,0 +1,36 @@ +import 'package:nostr/nostr.dart'; +import 'package:test/test.dart'; + +void main() { + group('nip010', () { + test('fromTags', () { + List> tags = [ + ["e", '91cf9..4e5ca', 'wss://alicerelay.com', "root"], + ["e", '14aeb..8dad4', 'wss://bobrelay.com/nostr', "reply"], + ["p", '612ae..e610f', 'ws://carolrelay.com/ws'], + ]; + Thread thread = Nip10.fromTags(tags); + expect(thread.root.eventId, '91cf9..4e5ca'); + expect(thread.root.relayURL, 'wss://alicerelay.com'); + expect(thread.root.marker, 'root'); + + expect(thread.etags[0].eventId, '14aeb..8dad4'); + expect(thread.etags[0].relayURL, 'wss://bobrelay.com/nostr'); + expect(thread.etags[0].marker, 'reply'); + + expect(thread.ptags[0].pubkey, '612ae..e610f'); + expect(thread.ptags[0].relayURL, 'ws://carolrelay.com/ws'); + }); + + test('toTags', () { + ETag root = Nip10.rootTag('91cf9..4e5ca', 'wss://alicerelay.com'); + ETag eTag = ETag("14aeb..8dad4", "wss://bobrelay.com/nostr", "reply"); + PTag pTag = PTag("612ae..e610f", "ws://carolrelay.com/ws"); + Thread thread = Thread(root, [eTag], [pTag]); + + expect(thread.root.eventId, '91cf9..4e5ca'); + expect(thread.etags[0].eventId, "14aeb..8dad4"); + expect(thread.ptags[0].pubkey, "612ae..e610f"); + }); + }); +} diff --git a/test/nips/nip_019_test.dart b/test/nips/nip_019_test.dart new file mode 100644 index 0000000..2bcfa6d --- /dev/null +++ b/test/nips/nip_019_test.dart @@ -0,0 +1,38 @@ +import 'package:nostr/nostr.dart'; +import 'package:test/test.dart'; + +void main() { + group('nip019', () { + test('encode', () { + String encodedPrivkey = Nip19.encodePrivkey( + "5ee1c8000ab28edd64d74a7d951ac2dd559814887b1b9e1ac7c5f89e96125c12"); + String encodedPubkey = Nip19.encodePubkey( + "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d"); + String encodedNote = Nip19.encodeNote( + "2739cccdc3fa943ad447378e234ef1325a76f023a169b483b6fe8cab47a793e1"); + + expect(encodedPrivkey, + 'nsec1tmsusqq2k28d6exhff7e2xkzm42es9yg0vdeuxk8chufa9sjtsfq8z3spp'); + expect(encodedPubkey, + 'npub180cvv07tjdrrgpa0j7j7tmnyl2yr6yr7l8j4s3evf6u64th6gkwsyjh6w6'); + expect(encodedNote, + 'note1yuuuenwrl22r44z8x78zxnh3xfd8dupr595mfqakl6x2k3a8j0ssj0w27g'); + }); + + test('decode', () { + String privkey = Nip19.decodePrivkey( + "nsec1tmsusqq2k28d6exhff7e2xkzm42es9yg0vdeuxk8chufa9sjtsfq8z3spp"); + String pubkey = Nip19.decodePubkey( + "npub180cvv07tjdrrgpa0j7j7tmnyl2yr6yr7l8j4s3evf6u64th6gkwsyjh6w6"); + String note = Nip19.decodeNote( + "note1yuuuenwrl22r44z8x78zxnh3xfd8dupr595mfqakl6x2k3a8j0ssj0w27g"); + + expect(privkey, + '5ee1c8000ab28edd64d74a7d951ac2dd559814887b1b9e1ac7c5f89e96125c12'); + expect(pubkey, + '3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d'); + expect(note, + '2739cccdc3fa943ad447378e234ef1325a76f023a169b483b6fe8cab47a793e1'); + }); + }); +} diff --git a/test/nips/nip_020_test.dart b/test/nips/nip_020_test.dart new file mode 100644 index 0000000..3588225 --- /dev/null +++ b/test/nips/nip_020_test.dart @@ -0,0 +1,14 @@ +import 'package:nostr/nostr.dart'; +import 'package:test/test.dart'; + +void main() { + group('nip020', () { + test('getOk', () { + String okPayload = + '["OK", "b1a649ebe8b435ec71d3784793f3bbf4b93e64e17568a741aecd4c7ddeafce30", true, ""]'; + OKEvent? okEvent = Nip20.getOk(okPayload); + expect(okEvent!.status, true); + }); + + }); +} diff --git a/test/nips/nip_028_test.dart b/test/nips/nip_028_test.dart new file mode 100644 index 0000000..1dc0cf2 --- /dev/null +++ b/test/nips/nip_028_test.dart @@ -0,0 +1,69 @@ +import 'dart:math'; + +import 'package:nostr/nostr.dart'; +import 'package:test/test.dart'; + +void main() { + group('nip028', () { + test('createChannel', () { + String privkey = + "826ef0e93c1278bd89945377fadb6b6b51d9eedf74ecdb64a96f1897bb670be8"; + Event event = Nip28.createChannel('name', 'about', 'http://image.jpg', {'badges':'0f76c800a7ea76b83a3ae87de94c6046b98311bda8885cedd8420885b50de181'}, privkey); + Channel channel = Nip28.getChannelCreation(event); + print(event.serialize()); + expect(channel.picture, 'http://image.jpg'); + expect(channel.additional['badges'], '0f76c800a7ea76b83a3ae87de94c6046b98311bda8885cedd8420885b50de181'); + }); + + test('setMetadata', () { + String privkey = + "826ef0e93c1278bd89945377fadb6b6b51d9eedf74ecdb64a96f1897bb670be8"; + Event event = Nip28.setChannelMetaData('name', 'about', 'http://image.jpg', {'badges':'0f76c800a7ea76b83a3ae87de94c6046b98311bda8885cedd8420885b50de181'}, 'b83a3326b63470df6a86dca9456184e09ea1a237b2b41b36e0af740badf329e9','wss://example.com', privkey); + Channel channel = Nip28.getChannelMetadata(event); + expect(channel.channelId, "b83a3326b63470df6a86dca9456184e09ea1a237b2b41b36e0af740badf329e9"); + expect(channel.picture, 'http://image.jpg'); + expect(channel.additional['badges'], '0f76c800a7ea76b83a3ae87de94c6046b98311bda8885cedd8420885b50de181'); + }); + + test('sendChannelMessage', () { + String privkey = + "826ef0e93c1278bd89945377fadb6b6b51d9eedf74ecdb64a96f1897bb670be8"; + Event event = Nip28.sendChannelMessage("b83a3326b63470df6a86dca9456184e09ea1a237b2b41b36e0af740badf329e9", 'content', privkey); + ChannelMessage channelMessage = Nip28.getChannelMessage(event); + + expect(channelMessage.channelId, "b83a3326b63470df6a86dca9456184e09ea1a237b2b41b36e0af740badf329e9"); + expect(channelMessage.content, 'content'); + + /// reply & p + ETag eTag = ETag('0f76c800a7ea76b83a3ae87de94c6046b98311bda8885cedd8420885b50de181', "wss://example.com", 'reply'); + PTag pTag = PTag('2d38a56c4303bc722370c50c86fc8dd3327f06a8fe59b3ff3d670738d71dd1e1', "wss://example.com"); + Event event2 = Nip28.sendChannelMessage("b83a3326b63470df6a86dca9456184e09ea1a237b2b41b36e0af740badf329e9", 'content', privkey, etags: [eTag], ptags: [pTag]); + ChannelMessage channelMessage2 = Nip28.getChannelMessage(event2); + + expect(channelMessage2.channelId, "b83a3326b63470df6a86dca9456184e09ea1a237b2b41b36e0af740badf329e9"); + expect(channelMessage2.content, 'content'); + expect(channelMessage2.thread.etags[0].eventId, '0f76c800a7ea76b83a3ae87de94c6046b98311bda8885cedd8420885b50de181'); + expect(channelMessage2.thread.ptags[0].pubkey, '2d38a56c4303bc722370c50c86fc8dd3327f06a8fe59b3ff3d670738d71dd1e1'); + }); + + test('hideChannelMessage', () { + String privkey = + "826ef0e93c1278bd89945377fadb6b6b51d9eedf74ecdb64a96f1897bb670be8"; + Event event = Nip28.hideChannelMessage("b83a3326b63470df6a86dca9456184e09ea1a237b2b41b36e0af740badf329e9", "reason", privkey); + ChannelMessageHidden channelMessageHidden = Nip28.getMessageHidden(event); + + expect(channelMessageHidden.operator, '2d38a56c4303bc722370c50c86fc8dd3327f06a8fe59b3ff3d670738d71dd1e1'); + expect(channelMessageHidden.messageId, 'b83a3326b63470df6a86dca9456184e09ea1a237b2b41b36e0af740badf329e9'); + }); + + test('muteUser', () { + String privkey = + "826ef0e93c1278bd89945377fadb6b6b51d9eedf74ecdb64a96f1897bb670be8"; + Event event = Nip28.muteUser("b83a3326b63470df6a86dca9456184e09ea1a237b2b41b36e0af740badf329e9", "reason", privkey); + ChannelUserMuted channelUserMuted = Nip28.getUserMuted(event); + + expect(channelUserMuted.operator, '2d38a56c4303bc722370c50c86fc8dd3327f06a8fe59b3ff3d670738d71dd1e1'); + expect(channelUserMuted.userPubkey, 'b83a3326b63470df6a86dca9456184e09ea1a237b2b41b36e0af740badf329e9'); + }); + }); +} diff --git a/test/nips/nip_051_test.dart b/test/nips/nip_051_test.dart new file mode 100644 index 0000000..b398958 --- /dev/null +++ b/test/nips/nip_051_test.dart @@ -0,0 +1,73 @@ +import 'package:nostr/nostr.dart'; +import 'package:test/test.dart'; + +void main() { + group('nip051', () { + test('createCategorizedPeople', () { + Keychain user = Keychain.generate(); + People publicFriend = People( + "2d38a56c4303bc722370c50c86fc8dd3327f06a8fe59b3ff3d670738d71dd1e1", + 'wss://example.com', + 'alias', + "0f76c800a7ea76b83a3ae87de94c6046b98311bda8885cedd8420885b50de181"); + People privateFriend = People( + "0f76c800a7ea76b83a3ae87de94c6046b98311bda8885cedd8420885b50de181", + 'wss://example2.com', + 'bob', + "2d38a56c4303bc722370c50c86fc8dd3327f06a8fe59b3ff3d670738d71dd1e1"); + Event event = Nip51.createCategorizedPeople("friends", [publicFriend], + [privateFriend], user.private, user.public); + + Lists lists = Nip51.getLists(event, user.private); + expect(lists.people[0].pubkey, '2d38a56c4303bc722370c50c86fc8dd3327f06a8fe59b3ff3d670738d71dd1e1'); + expect(lists.people[0].petName, 'alias'); + expect(lists.people[1].pubkey, '0f76c800a7ea76b83a3ae87de94c6046b98311bda8885cedd8420885b50de181'); + expect(lists.people[1].petName, 'bob'); + }); + + test('createCategorizedBookmarks', () { + Keychain user = Keychain.generate(); + String bookmark = '2d38a56c4303bc722370c50c86fc8dd3327f06a8fe59b3ff3d670738d71dd1e1'; + String encryptedBookmark = '0f76c800a7ea76b83a3ae87de94c6046b98311bda8885cedd8420885b50de181'; + Event event = Nip51.createCategorizedBookmarks("bookmarks", [bookmark], + [encryptedBookmark], user.private, user.public); + + Lists lists = Nip51.getLists(event, user.private); + expect(lists.bookmarks[0], '2d38a56c4303bc722370c50c86fc8dd3327f06a8fe59b3ff3d670738d71dd1e1'); + expect(lists.bookmarks[1], '0f76c800a7ea76b83a3ae87de94c6046b98311bda8885cedd8420885b50de181'); + }); + + test('createMutePeople', () { + Keychain user = Keychain.generate(); + People publicFriend = People( + "2d38a56c4303bc722370c50c86fc8dd3327f06a8fe59b3ff3d670738d71dd1e1", + 'wss://example.com', + 'alias', + "0f76c800a7ea76b83a3ae87de94c6046b98311bda8885cedd8420885b50de181"); + People privateFriend = People( + "0f76c800a7ea76b83a3ae87de94c6046b98311bda8885cedd8420885b50de181", + 'wss://example2.com', + 'bob', + "2d38a56c4303bc722370c50c86fc8dd3327f06a8fe59b3ff3d670738d71dd1e1"); + Event event = Nip51.createMutePeople([publicFriend], + [privateFriend], user.private, user.public); + Lists lists = Nip51.getLists(event, user.private); + expect(lists.people[0].pubkey, '2d38a56c4303bc722370c50c86fc8dd3327f06a8fe59b3ff3d670738d71dd1e1'); + expect(lists.people[0].petName, 'alias'); + expect(lists.people[1].pubkey, '0f76c800a7ea76b83a3ae87de94c6046b98311bda8885cedd8420885b50de181'); + expect(lists.people[1].petName, 'bob'); + }); + + test('createPinEvent', () { + Keychain user = Keychain.generate(); + String bookmark = '2d38a56c4303bc722370c50c86fc8dd3327f06a8fe59b3ff3d670738d71dd1e1'; + String encryptedBookmark = '0f76c800a7ea76b83a3ae87de94c6046b98311bda8885cedd8420885b50de181'; + Event event = Nip51.createPinEvent([bookmark], + [encryptedBookmark], user.private, user.public); + + Lists lists = Nip51.getLists(event, user.private); + expect(lists.bookmarks[0], '2d38a56c4303bc722370c50c86fc8dd3327f06a8fe59b3ff3d670738d71dd1e1'); + expect(lists.bookmarks[1], '0f76c800a7ea76b83a3ae87de94c6046b98311bda8885cedd8420885b50de181'); + }); + }); +} From 08787816d9490fecee17d635e0d797136d5681ee Mon Sep 17 00:00:00 2001 From: ethicnology Date: Wed, 24 May 2023 15:38:36 +0200 Subject: [PATCH 08/16] refactor: nip20 --- lib/src/message.dart | 3 +++ lib/src/nips/nip_020.dart | 49 +++++++++++++++++++++++++------------ test/nips/nip_020_test.dart | 38 +++++++++++++++++++++++----- 3 files changed, 69 insertions(+), 21 deletions(-) diff --git a/lib/src/message.dart b/lib/src/message.dart index 7cdfc00..99acf5a 100644 --- a/lib/src/message.dart +++ b/lib/src/message.dart @@ -19,6 +19,9 @@ class Message { message = Event.deserialize(data); if (message.kind == 4) message = EncryptedDirectMessage(message); break; + case "OK": + message = Nip20.deserialize(data); + break; case "REQ": message = Request.deserialize(data); break; diff --git a/lib/src/nips/nip_020.dart b/lib/src/nips/nip_020.dart index cf75f0f..7b51506 100644 --- a/lib/src/nips/nip_020.dart +++ b/lib/src/nips/nip_020.dart @@ -1,20 +1,39 @@ import 'dart:convert'; -import 'package:nostr/nostr.dart'; +/// When submitting events to relays, clients currently have no way to know if an event was successfully committed to the database. +/// This NIP introduces the concept of command results which are like NOTICE's except provide more information about if an event was accepted or rejected. +/// +/// Event successfully written to the database: +/// +/// ["OK", "b1a649ebe8b435ec71d3784793f3bbf4b93e64e17568a741aecd4c7ddeafce30", true, ""] +/// +/// Event successfully written to the database because of a reason: +/// +/// ["OK", "b1a649ebe8b435ec71d3784793f3bbf4b93e64e17568a741aecd4c7ddeafce30", true, "pow: difficulty 25>=24"] +/// +/// Event blocked due to ip filter: +/// +/// ["OK", "b1a649ebe8...", false, "blocked: tor exit nodes not allowed"] +/// +/// … class Nip20 { - static OKEvent? getOk(String okPayload) { - var ok = Message.deserialize(okPayload); - if(ok.type == 'OK'){ - var object = jsonDecode(ok.message); - return OKEvent(object[0], object[1], object[2]); - } - } -} + late String eventId; + late bool status; + late String message; + + /// Default constructor + Nip20(this.eventId, this.status, this.message); -class OKEvent { - String eventId; - bool status; - String message; + /// Serialize to nostr close message + /// - ["OK", "event_id", true|false, "message"] + String serialize() => jsonEncode(["OK", eventId, status, message]); - OKEvent(this.eventId, this.status, this.message); -} \ No newline at end of file + /// Deserialize a nostr close message + /// - ["OK", "event_id", true|false, "message"] + Nip20.deserialize(input) { + assert(input.length == 4); + eventId = input[1]; + status = input[2]; + message = input[3]; + } +} diff --git a/test/nips/nip_020_test.dart b/test/nips/nip_020_test.dart index 3588225..a4e643a 100644 --- a/test/nips/nip_020_test.dart +++ b/test/nips/nip_020_test.dart @@ -2,13 +2,39 @@ import 'package:nostr/nostr.dart'; import 'package:test/test.dart'; void main() { - group('nip020', () { - test('getOk', () { - String okPayload = - '["OK", "b1a649ebe8b435ec71d3784793f3bbf4b93e64e17568a741aecd4c7ddeafce30", true, ""]'; - OKEvent? okEvent = Nip20.getOk(okPayload); - expect(okEvent!.status, true); + group('nip20', () { + test('Constructor', () { + var eventId = + "b1a649ebe8b435ec71d3784793f3bbf4b93e64e17568a741aecd4c7ddeafce30"; + var status = true; + var message = ""; + var nip20 = Nip20(eventId, status, message); + expect(nip20.eventId, eventId); + expect(nip20.status, status); + expect(nip20.message, message); }); + test('serialize', () { + var eventId = + "b1a649ebe8b435ec71d3784793f3bbf4b93e64e17568a741aecd4c7ddeafce30"; + var status = true; + var message = ""; + var stringNip20 = '["OK","$eventId",$status,"$message"]'; + var nip20 = Nip20(eventId, status, message); + expect(nip20.serialize(), stringNip20); + }); + + test('deserialize', () { + var jsonNip20 = [ + "OK", + "b1a649ebe8b435ec71d3784793f3bbf4b93e64e17568a741aecd4c7ddeafce30", + true, + "" + ]; + var nip20 = Nip20.deserialize(jsonNip20); + expect(nip20.eventId, jsonNip20[1]); + expect(nip20.status, jsonNip20[2]); + expect(nip20.message, jsonNip20[3]); + }); }); } From ee41a49a3707fae663f425bd3a1a13e40f0ecffc Mon Sep 17 00:00:00 2001 From: ethicnology Date: Wed, 24 May 2023 15:49:54 +0200 Subject: [PATCH 09/16] fix: useless import --- test/nips/nip_028_test.dart | 88 ++++++++++++++++++++++++++++--------- 1 file changed, 67 insertions(+), 21 deletions(-) diff --git a/test/nips/nip_028_test.dart b/test/nips/nip_028_test.dart index 1dc0cf2..3ed9cea 100644 --- a/test/nips/nip_028_test.dart +++ b/test/nips/nip_028_test.dart @@ -1,5 +1,3 @@ -import 'dart:math'; - import 'package:nostr/nostr.dart'; import 'package:test/test.dart'; @@ -8,62 +6,110 @@ void main() { test('createChannel', () { String privkey = "826ef0e93c1278bd89945377fadb6b6b51d9eedf74ecdb64a96f1897bb670be8"; - Event event = Nip28.createChannel('name', 'about', 'http://image.jpg', {'badges':'0f76c800a7ea76b83a3ae87de94c6046b98311bda8885cedd8420885b50de181'}, privkey); + Event event = Nip28.createChannel( + 'name', + 'about', + 'http://image.jpg', + { + 'badges': + '0f76c800a7ea76b83a3ae87de94c6046b98311bda8885cedd8420885b50de181' + }, + privkey); Channel channel = Nip28.getChannelCreation(event); print(event.serialize()); expect(channel.picture, 'http://image.jpg'); - expect(channel.additional['badges'], '0f76c800a7ea76b83a3ae87de94c6046b98311bda8885cedd8420885b50de181'); + expect(channel.additional['badges'], + '0f76c800a7ea76b83a3ae87de94c6046b98311bda8885cedd8420885b50de181'); }); test('setMetadata', () { String privkey = "826ef0e93c1278bd89945377fadb6b6b51d9eedf74ecdb64a96f1897bb670be8"; - Event event = Nip28.setChannelMetaData('name', 'about', 'http://image.jpg', {'badges':'0f76c800a7ea76b83a3ae87de94c6046b98311bda8885cedd8420885b50de181'}, 'b83a3326b63470df6a86dca9456184e09ea1a237b2b41b36e0af740badf329e9','wss://example.com', privkey); + Event event = Nip28.setChannelMetaData( + 'name', + 'about', + 'http://image.jpg', + { + 'badges': + '0f76c800a7ea76b83a3ae87de94c6046b98311bda8885cedd8420885b50de181' + }, + 'b83a3326b63470df6a86dca9456184e09ea1a237b2b41b36e0af740badf329e9', + 'wss://example.com', + privkey); Channel channel = Nip28.getChannelMetadata(event); - expect(channel.channelId, "b83a3326b63470df6a86dca9456184e09ea1a237b2b41b36e0af740badf329e9"); + expect(channel.channelId, + "b83a3326b63470df6a86dca9456184e09ea1a237b2b41b36e0af740badf329e9"); expect(channel.picture, 'http://image.jpg'); - expect(channel.additional['badges'], '0f76c800a7ea76b83a3ae87de94c6046b98311bda8885cedd8420885b50de181'); + expect(channel.additional['badges'], + '0f76c800a7ea76b83a3ae87de94c6046b98311bda8885cedd8420885b50de181'); }); test('sendChannelMessage', () { String privkey = "826ef0e93c1278bd89945377fadb6b6b51d9eedf74ecdb64a96f1897bb670be8"; - Event event = Nip28.sendChannelMessage("b83a3326b63470df6a86dca9456184e09ea1a237b2b41b36e0af740badf329e9", 'content', privkey); + Event event = Nip28.sendChannelMessage( + "b83a3326b63470df6a86dca9456184e09ea1a237b2b41b36e0af740badf329e9", + 'content', + privkey); ChannelMessage channelMessage = Nip28.getChannelMessage(event); - expect(channelMessage.channelId, "b83a3326b63470df6a86dca9456184e09ea1a237b2b41b36e0af740badf329e9"); + expect(channelMessage.channelId, + "b83a3326b63470df6a86dca9456184e09ea1a237b2b41b36e0af740badf329e9"); expect(channelMessage.content, 'content'); /// reply & p - ETag eTag = ETag('0f76c800a7ea76b83a3ae87de94c6046b98311bda8885cedd8420885b50de181', "wss://example.com", 'reply'); - PTag pTag = PTag('2d38a56c4303bc722370c50c86fc8dd3327f06a8fe59b3ff3d670738d71dd1e1', "wss://example.com"); - Event event2 = Nip28.sendChannelMessage("b83a3326b63470df6a86dca9456184e09ea1a237b2b41b36e0af740badf329e9", 'content', privkey, etags: [eTag], ptags: [pTag]); + ETag eTag = ETag( + '0f76c800a7ea76b83a3ae87de94c6046b98311bda8885cedd8420885b50de181', + "wss://example.com", + 'reply'); + PTag pTag = PTag( + '2d38a56c4303bc722370c50c86fc8dd3327f06a8fe59b3ff3d670738d71dd1e1', + "wss://example.com"); + Event event2 = Nip28.sendChannelMessage( + "b83a3326b63470df6a86dca9456184e09ea1a237b2b41b36e0af740badf329e9", + 'content', + privkey, + etags: [eTag], + ptags: [pTag]); ChannelMessage channelMessage2 = Nip28.getChannelMessage(event2); - expect(channelMessage2.channelId, "b83a3326b63470df6a86dca9456184e09ea1a237b2b41b36e0af740badf329e9"); + expect(channelMessage2.channelId, + "b83a3326b63470df6a86dca9456184e09ea1a237b2b41b36e0af740badf329e9"); expect(channelMessage2.content, 'content'); - expect(channelMessage2.thread.etags[0].eventId, '0f76c800a7ea76b83a3ae87de94c6046b98311bda8885cedd8420885b50de181'); - expect(channelMessage2.thread.ptags[0].pubkey, '2d38a56c4303bc722370c50c86fc8dd3327f06a8fe59b3ff3d670738d71dd1e1'); + expect(channelMessage2.thread.etags[0].eventId, + '0f76c800a7ea76b83a3ae87de94c6046b98311bda8885cedd8420885b50de181'); + expect(channelMessage2.thread.ptags[0].pubkey, + '2d38a56c4303bc722370c50c86fc8dd3327f06a8fe59b3ff3d670738d71dd1e1'); }); test('hideChannelMessage', () { String privkey = "826ef0e93c1278bd89945377fadb6b6b51d9eedf74ecdb64a96f1897bb670be8"; - Event event = Nip28.hideChannelMessage("b83a3326b63470df6a86dca9456184e09ea1a237b2b41b36e0af740badf329e9", "reason", privkey); + Event event = Nip28.hideChannelMessage( + "b83a3326b63470df6a86dca9456184e09ea1a237b2b41b36e0af740badf329e9", + "reason", + privkey); ChannelMessageHidden channelMessageHidden = Nip28.getMessageHidden(event); - expect(channelMessageHidden.operator, '2d38a56c4303bc722370c50c86fc8dd3327f06a8fe59b3ff3d670738d71dd1e1'); - expect(channelMessageHidden.messageId, 'b83a3326b63470df6a86dca9456184e09ea1a237b2b41b36e0af740badf329e9'); + expect(channelMessageHidden.operator, + '2d38a56c4303bc722370c50c86fc8dd3327f06a8fe59b3ff3d670738d71dd1e1'); + expect(channelMessageHidden.messageId, + 'b83a3326b63470df6a86dca9456184e09ea1a237b2b41b36e0af740badf329e9'); }); test('muteUser', () { String privkey = "826ef0e93c1278bd89945377fadb6b6b51d9eedf74ecdb64a96f1897bb670be8"; - Event event = Nip28.muteUser("b83a3326b63470df6a86dca9456184e09ea1a237b2b41b36e0af740badf329e9", "reason", privkey); + Event event = Nip28.muteUser( + "b83a3326b63470df6a86dca9456184e09ea1a237b2b41b36e0af740badf329e9", + "reason", + privkey); ChannelUserMuted channelUserMuted = Nip28.getUserMuted(event); - expect(channelUserMuted.operator, '2d38a56c4303bc722370c50c86fc8dd3327f06a8fe59b3ff3d670738d71dd1e1'); - expect(channelUserMuted.userPubkey, 'b83a3326b63470df6a86dca9456184e09ea1a237b2b41b36e0af740badf329e9'); + expect(channelUserMuted.operator, + '2d38a56c4303bc722370c50c86fc8dd3327f06a8fe59b3ff3d670738d71dd1e1'); + expect(channelUserMuted.userPubkey, + 'b83a3326b63470df6a86dca9456184e09ea1a237b2b41b36e0af740badf329e9'); }); }); } From 6fdad5c9c842a8cbbffd3e6ec835543ea93a0f46 Mon Sep 17 00:00:00 2001 From: ethicnology Date: Wed, 24 May 2023 15:50:10 +0200 Subject: [PATCH 10/16] chore: format --- lib/nostr.dart | 1 - lib/src/nips/nip_005.dart | 3 ++- test/nips/nip_010_test.dart | 8 +++---- test/nips/nip_051_test.dart | 46 +++++++++++++++++++++++-------------- 4 files changed, 35 insertions(+), 23 deletions(-) diff --git a/lib/nostr.dart b/lib/nostr.dart index f91d30f..124c051 100644 --- a/lib/nostr.dart +++ b/lib/nostr.dart @@ -19,4 +19,3 @@ export 'src/nips/nip_019.dart'; export 'src/nips/nip_020.dart'; export 'src/nips/nip_028.dart'; export 'src/nips/nip_051.dart'; - diff --git a/lib/src/nips/nip_005.dart b/lib/src/nips/nip_005.dart index 9a9e578..59e9e21 100644 --- a/lib/src/nips/nip_005.dart +++ b/lib/src/nips/nip_005.dart @@ -19,7 +19,8 @@ class Nip5 { List parts = dns.split('@'); String name = parts[0]; String domain = parts[1]; - return DNS(name, domain, event.pubkey, relays.map((e) => e.toString()).toList()); + return DNS(name, domain, event.pubkey, + relays.map((e) => e.toString()).toList()); } } catch (e) { throw Exception(e.toString()); diff --git a/test/nips/nip_010_test.dart b/test/nips/nip_010_test.dart index 79e2105..e74ecea 100644 --- a/test/nips/nip_010_test.dart +++ b/test/nips/nip_010_test.dart @@ -5,10 +5,10 @@ void main() { group('nip010', () { test('fromTags', () { List> tags = [ - ["e", '91cf9..4e5ca', 'wss://alicerelay.com', "root"], - ["e", '14aeb..8dad4', 'wss://bobrelay.com/nostr', "reply"], - ["p", '612ae..e610f', 'ws://carolrelay.com/ws'], - ]; + ["e", '91cf9..4e5ca', 'wss://alicerelay.com', "root"], + ["e", '14aeb..8dad4', 'wss://bobrelay.com/nostr', "reply"], + ["p", '612ae..e610f', 'ws://carolrelay.com/ws'], + ]; Thread thread = Nip10.fromTags(tags); expect(thread.root.eventId, '91cf9..4e5ca'); expect(thread.root.relayURL, 'wss://alicerelay.com'); diff --git a/test/nips/nip_051_test.dart b/test/nips/nip_051_test.dart index b398958..b7913b7 100644 --- a/test/nips/nip_051_test.dart +++ b/test/nips/nip_051_test.dart @@ -17,24 +17,30 @@ void main() { "2d38a56c4303bc722370c50c86fc8dd3327f06a8fe59b3ff3d670738d71dd1e1"); Event event = Nip51.createCategorizedPeople("friends", [publicFriend], [privateFriend], user.private, user.public); - + Lists lists = Nip51.getLists(event, user.private); - expect(lists.people[0].pubkey, '2d38a56c4303bc722370c50c86fc8dd3327f06a8fe59b3ff3d670738d71dd1e1'); + expect(lists.people[0].pubkey, + '2d38a56c4303bc722370c50c86fc8dd3327f06a8fe59b3ff3d670738d71dd1e1'); expect(lists.people[0].petName, 'alias'); - expect(lists.people[1].pubkey, '0f76c800a7ea76b83a3ae87de94c6046b98311bda8885cedd8420885b50de181'); + expect(lists.people[1].pubkey, + '0f76c800a7ea76b83a3ae87de94c6046b98311bda8885cedd8420885b50de181'); expect(lists.people[1].petName, 'bob'); }); test('createCategorizedBookmarks', () { Keychain user = Keychain.generate(); - String bookmark = '2d38a56c4303bc722370c50c86fc8dd3327f06a8fe59b3ff3d670738d71dd1e1'; - String encryptedBookmark = '0f76c800a7ea76b83a3ae87de94c6046b98311bda8885cedd8420885b50de181'; + String bookmark = + '2d38a56c4303bc722370c50c86fc8dd3327f06a8fe59b3ff3d670738d71dd1e1'; + String encryptedBookmark = + '0f76c800a7ea76b83a3ae87de94c6046b98311bda8885cedd8420885b50de181'; Event event = Nip51.createCategorizedBookmarks("bookmarks", [bookmark], [encryptedBookmark], user.private, user.public); Lists lists = Nip51.getLists(event, user.private); - expect(lists.bookmarks[0], '2d38a56c4303bc722370c50c86fc8dd3327f06a8fe59b3ff3d670738d71dd1e1'); - expect(lists.bookmarks[1], '0f76c800a7ea76b83a3ae87de94c6046b98311bda8885cedd8420885b50de181'); + expect(lists.bookmarks[0], + '2d38a56c4303bc722370c50c86fc8dd3327f06a8fe59b3ff3d670738d71dd1e1'); + expect(lists.bookmarks[1], + '0f76c800a7ea76b83a3ae87de94c6046b98311bda8885cedd8420885b50de181'); }); test('createMutePeople', () { @@ -49,25 +55,31 @@ void main() { 'wss://example2.com', 'bob', "2d38a56c4303bc722370c50c86fc8dd3327f06a8fe59b3ff3d670738d71dd1e1"); - Event event = Nip51.createMutePeople([publicFriend], - [privateFriend], user.private, user.public); + Event event = Nip51.createMutePeople( + [publicFriend], [privateFriend], user.private, user.public); Lists lists = Nip51.getLists(event, user.private); - expect(lists.people[0].pubkey, '2d38a56c4303bc722370c50c86fc8dd3327f06a8fe59b3ff3d670738d71dd1e1'); + expect(lists.people[0].pubkey, + '2d38a56c4303bc722370c50c86fc8dd3327f06a8fe59b3ff3d670738d71dd1e1'); expect(lists.people[0].petName, 'alias'); - expect(lists.people[1].pubkey, '0f76c800a7ea76b83a3ae87de94c6046b98311bda8885cedd8420885b50de181'); + expect(lists.people[1].pubkey, + '0f76c800a7ea76b83a3ae87de94c6046b98311bda8885cedd8420885b50de181'); expect(lists.people[1].petName, 'bob'); }); test('createPinEvent', () { Keychain user = Keychain.generate(); - String bookmark = '2d38a56c4303bc722370c50c86fc8dd3327f06a8fe59b3ff3d670738d71dd1e1'; - String encryptedBookmark = '0f76c800a7ea76b83a3ae87de94c6046b98311bda8885cedd8420885b50de181'; - Event event = Nip51.createPinEvent([bookmark], - [encryptedBookmark], user.private, user.public); + String bookmark = + '2d38a56c4303bc722370c50c86fc8dd3327f06a8fe59b3ff3d670738d71dd1e1'; + String encryptedBookmark = + '0f76c800a7ea76b83a3ae87de94c6046b98311bda8885cedd8420885b50de181'; + Event event = Nip51.createPinEvent( + [bookmark], [encryptedBookmark], user.private, user.public); Lists lists = Nip51.getLists(event, user.private); - expect(lists.bookmarks[0], '2d38a56c4303bc722370c50c86fc8dd3327f06a8fe59b3ff3d670738d71dd1e1'); - expect(lists.bookmarks[1], '0f76c800a7ea76b83a3ae87de94c6046b98311bda8885cedd8420885b50de181'); + expect(lists.bookmarks[0], + '2d38a56c4303bc722370c50c86fc8dd3327f06a8fe59b3ff3d670738d71dd1e1'); + expect(lists.bookmarks[1], + '0f76c800a7ea76b83a3ae87de94c6046b98311bda8885cedd8420885b50de181'); }); }); } From b0f4a53eb4156b946fa5aa175299d585ba7cea13 Mon Sep 17 00:00:00 2001 From: ethicnology Date: Wed, 24 May 2023 16:01:00 +0200 Subject: [PATCH 11/16] refactor: PR #29 --- lib/src/nips/nip_028.dart | 74 +++++++++++++++++++------------------ lib/src/nips/nip_051.dart | 2 +- test/nips/nip_005_test.dart | 1 - test/nips/nip_028_test.dart | 1 - 4 files changed, 40 insertions(+), 38 deletions(-) diff --git a/lib/src/nips/nip_028.dart b/lib/src/nips/nip_028.dart index d93ebd3..ba85de3 100644 --- a/lib/src/nips/nip_028.dart +++ b/lib/src/nips/nip_028.dart @@ -116,10 +116,12 @@ class Nip28 { 'picture': picture, }; map.addAll(additional); - String content = jsonEncode(map); - Event event = - Event.from(kind: 40, tags: [], content: content, privkey: privkey); - return event; + return Event.from( + kind: 40, + tags: [], + content: jsonEncode(map), + privkey: privkey, + ); } static Event setChannelMetaData( @@ -133,52 +135,54 @@ class Nip28 { Map map = { 'name': name, 'about': about, - 'picture': picture, + 'picture': picture }; map.addAll(additional); - String content = jsonEncode(map); - List> tags = []; - tags.add(["e", channelId, relayURL]); - Event event = - Event.from(kind: 41, tags: tags, content: content, privkey: privkey); - return event; + return Event.from( + kind: 41, + tags: [ + ["e", channelId, relayURL] + ], + content: jsonEncode(map), + privkey: privkey, + ); } static Event sendChannelMessage( String channelId, String content, String privkey, {String? relay, List? etags, List? ptags}) { - List> tags = []; - Thread t = + Thread thread = Thread(Nip10.rootTag(channelId, relay ?? ''), etags ?? [], ptags ?? []); - tags = Nip10.toTags(t); - Event event = - Event.from(kind: 42, tags: tags, content: content, privkey: privkey); - return event; + return Event.from( + kind: 42, + tags: Nip10.toTags(thread), + content: content, + privkey: privkey, + ); } static Event hideChannelMessage( String messageId, String reason, String privkey) { - Map map = { - 'reason': reason, - }; - String content = jsonEncode(map); - List> tags = []; - tags.add(["e", messageId]); - Event event = - Event.from(kind: 43, tags: tags, content: content, privkey: privkey); - return event; + Map map = {'reason': reason}; + return Event.from( + kind: 43, + tags: [ + ["e", messageId] + ], + content: jsonEncode(map), + privkey: privkey, + ); } static Event muteUser(String pubkey, String reason, String privkey) { - Map map = { - 'reason': reason, - }; - String content = jsonEncode(map); - List> tags = []; - tags.add(["p", pubkey]); - Event event = - Event.from(kind: 44, tags: tags, content: content, privkey: privkey); - return event; + Map map = {'reason': reason}; + return Event.from( + kind: 44, + tags: [ + ["p", pubkey] + ], + content: jsonEncode(map), + privkey: privkey); } } diff --git a/lib/src/nips/nip_051.dart b/lib/src/nips/nip_051.dart index f0483a3..52990f1 100644 --- a/lib/src/nips/nip_051.dart +++ b/lib/src/nips/nip_051.dart @@ -59,7 +59,7 @@ class Nip51 { List bookmarks = []; int ivIndex = content.indexOf("?iv="); if (ivIndex <= 0) { - print("Invalid content, could not get ivIndex: $content"); + throw Exception("Invalid content, could not get ivIndex: $content"); } String iv = content.substring(ivIndex + "?iv=".length, content.length); String encString = content.substring(0, ivIndex); diff --git a/test/nips/nip_005_test.dart b/test/nips/nip_005_test.dart index 7fab14a..215e794 100644 --- a/test/nips/nip_005_test.dart +++ b/test/nips/nip_005_test.dart @@ -12,7 +12,6 @@ void main() { 'wss://relay2.example.com' ]; Event event = Nip5.encode('name', 'example.com', relays, user.private); - print(event.serialize()); expect(event.kind, 0); expect(() => Nip5.encode('name', 'example', relays, user.private), diff --git a/test/nips/nip_028_test.dart b/test/nips/nip_028_test.dart index 3ed9cea..c5eb1e8 100644 --- a/test/nips/nip_028_test.dart +++ b/test/nips/nip_028_test.dart @@ -16,7 +16,6 @@ void main() { }, privkey); Channel channel = Nip28.getChannelCreation(event); - print(event.serialize()); expect(channel.picture, 'http://image.jpg'); expect(channel.additional['badges'], '0f76c800a7ea76b83a3ae87de94c6046b98311bda8885cedd8420885b50de181'); From f5ccd800a6e3c0ce3e55a61593ee1462b3993a6c Mon Sep 17 00:00:00 2001 From: hazeycode <22148308+hazeycode@users.noreply.github.com> Date: Sun, 21 May 2023 21:16:30 +0100 Subject: [PATCH 12/16] runtime data errors are exceptions not asserts --- lib/src/close.dart | 4 +++- lib/src/event.dart | 4 +++- lib/src/message.dart | 4 +++- lib/src/request.dart | 4 +++- 4 files changed, 12 insertions(+), 4 deletions(-) diff --git a/lib/src/close.dart b/lib/src/close.dart index fbbbe7a..34f08ba 100644 --- a/lib/src/close.dart +++ b/lib/src/close.dart @@ -17,7 +17,9 @@ class Close { /// Deserialize a nostr close message /// - ["CLOSE", subscription_id] Close.deserialize(input) { - assert(input.length == 2); + if (input.length != 2) { + throw 'Invalid length for CLOSE message'; + } subscriptionId = input[1]; } } diff --git a/lib/src/event.dart b/lib/src/event.dart index 96f7c25..c3c9de6 100644 --- a/lib/src/event.dart +++ b/lib/src/event.dart @@ -86,7 +86,9 @@ class Event { bool verify = true, }) { pubkey = pubkey.toLowerCase(); - if (verify) assert(isValid() == true); + if (verify && isValid() == false) { + throw 'Invalid event'; + } } /// Partial constructor, you have to fill the fields yourself diff --git a/lib/src/message.dart b/lib/src/message.dart index 99acf5a..a02cfcd 100644 --- a/lib/src/message.dart +++ b/lib/src/message.dart @@ -11,7 +11,9 @@ class Message { Message.deserialize(String payload) { dynamic data = jsonDecode(payload); var messages = ["EVENT", "REQ", "CLOSE", "NOTICE", "EOSE", "OK", "AUTH"]; - assert(messages.contains(data[0]), "Unsupported payload (or NIP)"); + if (messages.contains(data[0]) == false) { + throw 'Unsupported payload (or NIP)'; + } type = data[0]; switch (type) { diff --git a/lib/src/request.dart b/lib/src/request.dart index 26ca262..e229809 100644 --- a/lib/src/request.dart +++ b/lib/src/request.dart @@ -23,7 +23,9 @@ class Request { /// Deserialize a nostr request message /// - ["REQ", subscription_id, filter JSON, filter JSON, ...] Request.deserialize(input) { - assert(input.length >= 3); + if (input.length < 3) { + throw 'Message too short'; + } subscriptionId = input[1]; filters = []; for (var i = 2; i < input.length; i++) { From f66c9981f102178de61939a4f7d2f69c88de9bc8 Mon Sep 17 00:00:00 2001 From: ethicnology Date: Wed, 24 May 2023 16:13:25 +0200 Subject: [PATCH 13/16] refactor: PR #28 --- lib/src/close.dart | 4 +--- lib/src/event.dart | 4 +--- lib/src/request.dart | 4 +--- 3 files changed, 3 insertions(+), 9 deletions(-) diff --git a/lib/src/close.dart b/lib/src/close.dart index 34f08ba..5205226 100644 --- a/lib/src/close.dart +++ b/lib/src/close.dart @@ -17,9 +17,7 @@ class Close { /// Deserialize a nostr close message /// - ["CLOSE", subscription_id] Close.deserialize(input) { - if (input.length != 2) { - throw 'Invalid length for CLOSE message'; - } + if (input.length != 2) throw 'Invalid length for CLOSE message'; subscriptionId = input[1]; } } diff --git a/lib/src/event.dart b/lib/src/event.dart index c3c9de6..a126a07 100644 --- a/lib/src/event.dart +++ b/lib/src/event.dart @@ -86,9 +86,7 @@ class Event { bool verify = true, }) { pubkey = pubkey.toLowerCase(); - if (verify && isValid() == false) { - throw 'Invalid event'; - } + if (verify && isValid() == false) throw 'Invalid event'; } /// Partial constructor, you have to fill the fields yourself diff --git a/lib/src/request.dart b/lib/src/request.dart index e229809..6b71bfc 100644 --- a/lib/src/request.dart +++ b/lib/src/request.dart @@ -23,9 +23,7 @@ class Request { /// Deserialize a nostr request message /// - ["REQ", subscription_id, filter JSON, filter JSON, ...] Request.deserialize(input) { - if (input.length < 3) { - throw 'Message too short'; - } + if (input.length < 3) throw 'Message too short'; subscriptionId = input[1]; filters = []; for (var i = 2; i < input.length; i++) { From bfbe9177eeac4b165832dd305f1973374a8c840b Mon Sep 17 00:00:00 2001 From: ethicnology Date: Thu, 25 May 2023 17:21:58 +0200 Subject: [PATCH 14/16] chore: update documentation --- README.md | 6 ++++++ example/message_example.dart | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 7cb4d4d..23cdd37 100644 --- a/README.md +++ b/README.md @@ -17,8 +17,14 @@ flutter pub add nostr - [x] [NIP 01 Basic protocol flow description](https://github.com/nostr-protocol/nips/blob/master/01.md) - [x] [NIP 02 Contact List and Petnames](https://github.com/nostr-protocol/nips/blob/master/02.md) - [x] [NIP 04 Encrypted Direct Message](https://github.com/nostr-protocol/nips/blob/master/04.md) +- [x] [NIP 05 Mapping Nostr keys to DNS-based internet identifiers](https://github.com/nostr-protocol/nips/blob/master/05.md) +- [x] [NIP 10 Conventions for clients' use of e and p tags in text events](https://github.com/nostr-protocol/nips/blob/master/10.md) - [x] [NIP 15 End of Stored Events Notice](https://github.com/nostr-protocol/nips/blob/master/15.md) +- [x] [NIP 19 bech32-encoded entities](https://github.com/nostr-protocol/nips/blob/master/19.md) - [x] [NIP 20 Command Results](https://github.com/nostr-protocol/nips/blob/master/20.md) +- [x] [NIP 28 Public Chat](https://github.com/nostr-protocol/nips/blob/master/28.md) +- [x] [NIP 51 Lists](https://github.com/nostr-protocol/nips/blob/master/51.md) + ## Usage ### Events messages diff --git a/example/message_example.dart b/example/message_example.dart index 2321eab..d5df783 100644 --- a/example/message_example.dart +++ b/example/message_example.dart @@ -2,7 +2,7 @@ import 'package:nostr/nostr.dart'; void main() async { var eventPayload = - '["EVENT","3979053091133091",{"id":"a60679692533b308f1d862c2a5ca5c08a304e5157b1df5cde0ff0454b9920605","pubkey":"7c579328cf9028a4548d5117afa4f8448fb510ca9023f576b7bc90fc5be6ce7e","created_at":1674405882,"kind":1,"tags":[],"content":"GM gm gm! Currently bathing my brain in coffee ☕️ hahaha. How many other nostrinos love coffee? 🤪🤙","sig":"10262aa6a83e0b744cda2097f06f7354357512b82846f6ef23ef7d997136b64815c343b613a0635a27da7e628c96ac2475f66dd72513c1fb8ce6560824eb25b8"}]'; + '["EVENT","5ce1758166673a70e391303fb7cfeb0f5d47ec38a9342a27858950d13424d59b",{"content":"No quotes from the Bible? My global feed is full of religious nutcases","created_at":1685026912,"id":"e695c81fa5099b9f3ef0d868d8143eae481954114681bbe4432b50e44e199927","kind":1,"pubkey":"ab4103fc8cd4e1d8d31a99d079ed8293bdc26b11ec1ec61d95c13e43d7e048ff","sig":"0d17d6197ad12ab5ad77eb51231ae12c2ce1e639218bb6e3a01cce78aa092f3e77fb1f914b690675a425dcfd5b4dfa7be72c2cb608568798361781d75e354b32","tags":[["e","7804acd35bb9727d0374545a99bb4f30f901289aebaf3cf330dda28c235cd7ad"],["p","1bc70a0148b3f316da33fe3c89f23e3e71ac4ff998027ec712b905cd24f6a411"]]}]'; var event = Message.deserialize(eventPayload); assert(event.type == "EVENT"); assert(event.message.id == From 3057122afe7bc4be26168c75587b5dd33ab96161 Mon Sep 17 00:00:00 2001 From: ethicnology Date: Thu, 25 May 2023 21:15:39 +0200 Subject: [PATCH 15/16] refactor: mark NIP04 as deprecated to warn users about controversial discussions about its harmfullness --- lib/src/message.dart | 1 + lib/src/nips/nip_004.dart | 9 +++++++++ test/nips/nip_004_test.dart | 2 ++ 3 files changed, 12 insertions(+) diff --git a/lib/src/message.dart b/lib/src/message.dart index a02cfcd..b142a94 100644 --- a/lib/src/message.dart +++ b/lib/src/message.dart @@ -19,6 +19,7 @@ class Message { switch (type) { case "EVENT": message = Event.deserialize(data); + // ignore: deprecated_member_use_from_same_package if (message.kind == 4) message = EncryptedDirectMessage(message); break; case "OK": diff --git a/lib/src/nips/nip_004.dart b/lib/src/nips/nip_004.dart index aec1fc4..e0f777a 100644 --- a/lib/src/nips/nip_004.dart +++ b/lib/src/nips/nip_004.dart @@ -14,6 +14,15 @@ import 'package:nostr/src/utils.dart'; /// tags MAY contain an entry identifying the previous message in a conversation or a message we are explicitly replying to (such that contextual, more organized conversations may happen), in the form ["e", "event_id"]. /// /// Note: By default in the libsecp256k1 ECDH implementation, the secret is the SHA256 hash of the shared point (both X and Y coordinates). In Nostr, only the X coordinate of the shared point is used as the secret and it is NOT hashed. If using libsecp256k1, a custom function that copies the X coordinate must be passed as the hashfp argument in secp256k1_ecdh. +/// +/// NIP-04 considered harmful, READ: https://github.com/ethicnology/dart-nostr/issues/15 and https://github.com/nostr-protocol/nips/issues/107 +@Deprecated( + """ + NIP-04 a.k.a EncryptedDirectMessage is controversial, please READ: + - https://github.com/ethicnology/dart-nostr/issues/15 + - https://github.com/nostr-protocol/nips/issues/107 + """, +) class EncryptedDirectMessage extends Event { /// Default constructor EncryptedDirectMessage(Event event, {verify = true}) diff --git a/test/nips/nip_004_test.dart b/test/nips/nip_004_test.dart index 720a005..540f1d4 100644 --- a/test/nips/nip_004_test.dart +++ b/test/nips/nip_004_test.dart @@ -1,3 +1,5 @@ +// ignore_for_file: deprecated_member_use_from_same_package + import 'package:nostr/nostr.dart'; import 'package:test/test.dart'; From a52e2ea59348fb53fcf3bc0a6b0c5f2b24ccc046 Mon Sep 17 00:00:00 2001 From: ethicnology Date: Thu, 25 May 2023 22:01:50 +0200 Subject: [PATCH 16/16] refactor: remove utility functions setMetadata, textNote and recommendServer --- lib/nostr.dart | 1 - lib/src/nips/nip_001.dart | 19 ------------------- lib/src/nips/nip_005.dart | 2 +- 3 files changed, 1 insertion(+), 21 deletions(-) delete mode 100644 lib/src/nips/nip_001.dart diff --git a/lib/nostr.dart b/lib/nostr.dart index 124c051..ece862d 100644 --- a/lib/nostr.dart +++ b/lib/nostr.dart @@ -10,7 +10,6 @@ export 'src/filter.dart'; export 'src/close.dart'; export 'src/message.dart'; export 'src/utils.dart'; -export 'src/nips/nip_001.dart'; export 'src/nips/nip_002.dart'; export 'src/nips/nip_004.dart'; export 'src/nips/nip_005.dart'; diff --git a/lib/src/nips/nip_001.dart b/lib/src/nips/nip_001.dart deleted file mode 100644 index 5f0ec44..0000000 --- a/lib/src/nips/nip_001.dart +++ /dev/null @@ -1,19 +0,0 @@ -import 'package:nostr/nostr.dart'; - -/// Basic Event Kinds -/// 0: set_metadata: the content is set to a stringified JSON object {name: , about: , picture: } describing the user who created the event. A relay may delete past set_metadata events once it gets a new one for the same pubkey. -/// 1: text_note: the content is set to the plaintext content of a note (anything the user wants to say). Do not use Markdown! Clients should not have to guess how to interpret content like [](). Use different event kinds for parsable content. -/// 2: recommend_server: the content is set to the URL (e.g., wss://somerelay.com) of a relay the event creator wants to recommend to its followers. -class Nip1 { - static Event setMetadata(String content, String privkey) { - return Event.from(kind: 0, tags: [], content: content, privkey: privkey); - } - - static Event textNote(String content, String privkey) { - return Event.from(kind: 1, tags: [], content: content, privkey: privkey); - } - - static Event recommendServer(String content, String privkey) { - return Event.from(kind: 2, tags: [], content: content, privkey: privkey); - } -} diff --git a/lib/src/nips/nip_005.dart b/lib/src/nips/nip_005.dart index 59e9e21..db0611e 100644 --- a/lib/src/nips/nip_005.dart +++ b/lib/src/nips/nip_005.dart @@ -34,7 +34,7 @@ class Nip5 { String name, String domain, List relays, String privkey) { if (isValidName(name) && isValidDomain(domain)) { String content = generateContent(name, domain, relays); - return Nip1.setMetadata(content, privkey); + return Event.from(kind: 0, tags: [], content: content, privkey: privkey); } else { throw Exception("not a valid name or domain!"); }