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"); }); }); }