Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

1.4.0 #30

Merged
merged 16 commits into from
May 25, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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