Skip to content

Commit

Permalink
1.4.0 (#30)
Browse files Browse the repository at this point in the history
* 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

* fix: #25 warnings

* refactor: PR #25

* fix: decipher bug, add Event.quick (#26)

* fix encrypt bug, add Event.quick

* a send and a receive unit test

* refactor: PR #26

* chore: rename/reorganize files from #25 & #26

* feat: NIPS 1, 5, 10, 19, 20, 28, 51

* refactor: nip20

* fix: useless import

* chore: format

* refactor: PR #29

* runtime data errors are exceptions not asserts

* refactor: PR #28

* chore: update documentation

* refactor: mark NIP04 as deprecated to warn users about controversial discussions about its harmfullness

* refactor: remove utility functions setMetadata, textNote and recommendServer

---------

Co-authored-by: no-prob <113266379+no-prob@users.noreply.github.com>
Co-authored-by: water <130329555+water783@users.noreply.github.com>
Co-authored-by: hazeycode <22148308+hazeycode@users.noreply.github.com>
  • Loading branch information
4 people authored May 25, 2023
1 parent 044050b commit 22154ca
Show file tree
Hide file tree
Showing 27 changed files with 1,515 additions and 16 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
# Files and directories created by pub.
.dart_tool/
.packages
*.swp
x
tags

# Conventional directory for build outputs.
build/
Expand Down
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,15 @@ 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 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
Expand Down
2 changes: 1 addition & 1 deletion example/message_example.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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 ==
Expand Down
7 changes: 7 additions & 0 deletions lib/nostr.dart
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,10 @@ export 'src/close.dart';
export 'src/message.dart';
export 'src/utils.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';
2 changes: 1 addition & 1 deletion lib/src/close.dart
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ 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];
}
}
63 changes: 63 additions & 0 deletions lib/src/crypto/kepler.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// 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';

class Kepler {
/// return a Bytes data secret
static List<List<int>> 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'));
return [secretBytes.sublist(0, 32), secretBytes.sublist(32, 40)];
}

/// 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);
return scalarMultiple(privateKey.d!, publicKey.Q!);
}

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

/// 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<int> 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);
}
}
}
58 changes: 58 additions & 0 deletions lib/src/crypto/nip_004.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import 'dart:convert';
import 'dart:typed_data';
import 'package:nostr/nostr.dart';
import 'package:nostr/src/crypto/kepler.dart';
import 'package:pointycastle/export.dart';

/// NIP4 cipher
String nip4cipher(
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));
}

// params
List<List<int>> 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);

if (cipher) {
String stringIv = base64.encode(iv);
String plaintext = base64.encode(result);
return "$plaintext?iv=$stringIv";
} else {
return Utf8Decoder().convert(result);
}
}
113 changes: 113 additions & 0 deletions lib/src/crypto/operator.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
// credit: https://github.com/tjcampanella/kepler/blob/master/lib/src/operator.dart

import 'package:pointycastle/export.dart';

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();

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!;
}
19 changes: 10 additions & 9 deletions lib/src/event.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import 'dart:convert';
import 'dart:typed_data';
import 'package:convert/convert.dart';
import 'package:crypto/crypto.dart';
import 'package:pointycastle/export.dart';
import 'package:bip340/bip340.dart' as bip340;
import 'package:nostr/src/utils.dart';

Expand Down Expand Up @@ -85,7 +86,7 @@ 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
Expand Down Expand Up @@ -127,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<List<String>> tags,
List<List<String>> 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(
Expand Down Expand Up @@ -253,7 +253,7 @@ class Event {
throw Exception('invalid input');
}

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

Expand Down Expand Up @@ -302,7 +302,8 @@ class Event {
) {
List data = [0, pubkey.toLowerCase(), createdAt, kind, tags, content];
String serializedEvent = json.encode(data);
List<int> hash = sha256.convert(utf8.encode(serializedEvent)).bytes;
Uint8List hash = SHA256Digest()
.process(Uint8List.fromList(utf8.encode(serializedEvent)));
return hex.encode(hash);
}

Expand Down
9 changes: 8 additions & 1 deletion lib/src/message.dart
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,19 @@ 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) {
case "EVENT":
message = Event.deserialize(data);
// ignore: deprecated_member_use_from_same_package
if (message.kind == 4) message = EncryptedDirectMessage(message);
break;
case "OK":
message = Nip20.deserialize(data);
break;
case "REQ":
message = Request.deserialize(data);
Expand Down
Loading

0 comments on commit 22154ca

Please sign in to comment.