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

feat: IonConnect seal and gift wrap services #412

Merged
merged 15 commits into from
Dec 12, 2024
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
2 changes: 1 addition & 1 deletion .tool-versions
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
flutter 3.24.2
flutter 3.24.5
ruby 3.2.2
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,15 @@ class PrivateDirectMessageData with _$PrivateDirectMessageData, EntityMediaDataM
);
}

factory PrivateDirectMessageData.fromRawContent(String content) {
final parsedContent = TextParser.allMatchers().parse(content);

return PrivateDirectMessageData(
content: parsedContent,
media: {},
);
}

const PrivateDirectMessageData._();

FutureOr<EventMessage> toEventMessage({
Expand Down
3 changes: 3 additions & 0 deletions lib/app/features/nostr/model/nostr_entity.dart
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ class SimpleSigner implements EventSigner {
@override
final String publicKey;

@override
String get privateKey => '';

final String signature;

@override
Expand Down
1 change: 1 addition & 0 deletions lib/app/services/nostr/ed25519_key_store.dart
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ class Ed25519KeyStore with EventSigner {
@override
String get publicKey => hex.encode(_publicKeyBytes);

@override
String get privateKey => hex.encode(_privateKeyBytes);

final List<int> _publicKeyBytes;
Expand Down
74 changes: 74 additions & 0 deletions lib/app/services/nostr/ion_connect_gift_wrap_service.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// SPDX-License-Identifier: ice License 1.0

import 'dart:async';
import 'dart:convert';

import 'package:ion/app/features/feed/data/models/entities/related_pubkey.c.dart';
import 'package:ion/app/utils/date.dart';
import 'package:nip44/nip44.dart';
import 'package:nostr_dart/nostr_dart.dart';

abstract class IonConnectGiftWrapService {
Future<EventMessage> createWrap(
EventMessage event,
String pubkey,
EventSigner signer,
int contentKind,
);

Future<EventMessage> decodeWrap(
EventMessage wrap,
String pubkey,
EventSigner signer,
);
}

class IonConnectGiftWrapServiceImpl implements IonConnectGiftWrapService {
static const int wrapKind = 1059;

@override
Future<EventMessage> createWrap(
EventMessage event,
String pubkey,
EventSigner signer,
int contentKind,
) async {
final encryptedEvent = await Nip44.encryptMessage(
jsonEncode(event.toJson().last),
signer.privateKey,
pubkey,
);

final createdAt = randomDateBefore(
const Duration(days: 2),
);

return EventMessage.fromData(
signer: signer,
kind: wrapKind,
createdAt: createdAt,
content: encryptedEvent,
tags: [
[RelatedPubkey.tagName, pubkey],
['k', contentKind.toString()],
],
);
}

@override
Future<EventMessage> decodeWrap(
EventMessage wrap,
String pubkey,
EventSigner signer,
) async {
final decryptedContent = await Nip44.decryptMessage(
wrap.content,
signer.privateKey,
pubkey,
);

return EventMessage.fromPayloadJson(
jsonDecode(decryptedContent) as Map<String, dynamic>,
);
}
}
69 changes: 69 additions & 0 deletions lib/app/services/nostr/ion_connect_seal_service.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
// SPDX-License-Identifier: ice License 1.0

import 'dart:async';
import 'dart:convert';

import 'package:ion/app/utils/date.dart';
import 'package:nip44/nip44.dart';
import 'package:nostr_dart/nostr_dart.dart';

abstract class IOnConnectSealService {
Future<EventMessage> createSeal(
EventMessage rumor,
EventSigner signer,
String pubkey,
);

Future<EventMessage> decodeSeal(
EventMessage seal,
EventSigner signer,
String pubkey,
);
}

class IonConnectSealServiceImpl implements IOnConnectSealService {
static const int sealKind = 13;

@override
Future<EventMessage> createSeal(
EventMessage rumor,
EventSigner signer,
String pubkey,
) async {
final encodedRumor = jsonEncode(rumor.toJson().last);

final encryptedRumor = await Nip44.encryptMessage(
encodedRumor,
signer.privateKey,
pubkey,
);

final createdAt = randomDateBefore(
const Duration(days: 2),
);

return EventMessage.fromData(
signer: signer,
kind: sealKind,
createdAt: createdAt,
content: encryptedRumor,
);
}

@override
Future<EventMessage> decodeSeal(
EventMessage seal,
EventSigner signer,
String pubkey,
) async {
final decryptedContent = await Nip44.decryptMessage(
seal.content,
signer.privateKey,
pubkey,
);

return EventMessage.fromPayloadJson(
jsonDecode(decryptedContent) as Map<String, dynamic>,
);
}
}
17 changes: 17 additions & 0 deletions lib/app/utils/date.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
// SPDX-License-Identifier: ice License 1.0

import 'dart:math';

import 'package:flutter/cupertino.dart';
import 'package:intl/intl.dart';
import 'package:ion/app/extensions/build_context.dart';
Expand Down Expand Up @@ -124,3 +126,18 @@ String formatMessageTimestamp(DateTime dateTime) {
String formatDateToMonthDayYear(DateTime date) {
return DateFormat('MMMM d, yyyy').format(date);
}

/// Returns a random DateTime object before the current time.
///
/// [maxDuration]: The maximum duration before the current time.
/// Example:
/// ```dart
/// DateTime randomDate = randomDateBefore(const Duration(days: 2));
/// ```
///
DateTime randomDateBefore(Duration maxDuration) {
final now = DateTime.now();
final differenceInMilliseconds = now.difference(now.subtract(maxDuration)).inMilliseconds;
final randomMilliseconds = Random().nextInt(differenceInMilliseconds);
return now.subtract(Duration(milliseconds: randomMilliseconds));
}
35 changes: 30 additions & 5 deletions pubspec.lock
Original file line number Diff line number Diff line change
Expand Up @@ -366,6 +366,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.7.0"
cryptography_plus:
dependency: transitive
description:
name: cryptography_plus
sha256: "34db787df4f4740a39474b6fb0a610aa6dc13a5b5b68754b4787a79939ac0454"
url: "https://pub.dev"
source: hosted
version: "2.7.1"
csslib:
dependency: transitive
description:
Expand Down Expand Up @@ -518,6 +526,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.1.0"
elliptic:
dependency: transitive
description:
name: elliptic
sha256: "0c303d810603953a65dc39c4c542fb7538defd9e212403c54c266140819523b6"
url: "https://pub.dev"
source: hosted
version: "0.3.11"
email_validator:
dependency: "direct main"
description:
Expand Down Expand Up @@ -1376,15 +1392,24 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.0.0"
nip44:
dependency: "direct main"
description:
path: "."
ref: master
resolved-ref: "73379b8b1332f1ee78a250055494e6b73e673ea2"
url: "https://github.com/chebizarro/dart-nip44.git"
source: git
version: "1.0.0"
nostr_dart:
dependency: "direct main"
description:
path: "."
ref: "0.0.9"
resolved-ref: "1864965536d07d35b9fdef6131b28ca0a5123fde"
ref: "0.0.10"
resolved-ref: "2c32aa5672b5ae5e61ac9324f96e6b5646e29826"
url: "https://github.com/ice-blockchain/nostr-dart.git"
source: git
version: "0.0.9"
version: "0.0.10"
octo_image:
dependency: transitive
description:
Expand Down Expand Up @@ -2432,5 +2457,5 @@ packages:
source: hosted
version: "2.2.1"
sdks:
dart: ">=3.5.1 <4.0.0"
flutter: ">=3.24.2"
dart: ">=3.5.4 <4.0.0"
flutter: ">=3.24.5"
10 changes: 7 additions & 3 deletions pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ publish_to: "none"
version: 0.0.0+1

environment:
sdk: ">=3.4.1 <4.0.0"
flutter: "3.24.2"
sdk: ">=3.5.4 <4.0.0"
flutter: "3.24.5"
ice-orion marked this conversation as resolved.
Show resolved Hide resolved

dependencies:
audio_waveforms: ^1.1.1
Expand Down Expand Up @@ -59,10 +59,14 @@ dependencies:
markdown_quill: ^4.1.0
meta: ^1.15.0
mocktail: ^1.0.4
nip44:
git:
url: https://github.com/chebizarro/dart-nip44.git
ref: master
nostr_dart:
git:
url: https://github.com/ice-blockchain/nostr-dart.git
ref: 0.0.9
ref: 0.0.10
ogp_data_extract: ^0.1.4
package_info_plus: ^4.2.0
path: ^1.9.0
Expand Down
64 changes: 64 additions & 0 deletions test/app/services/nostr/ion_connect_gift_wrap_service_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
// SPDX-License-Identifier: ice License 1.0

import 'package:flutter_test/flutter_test.dart';
import 'package:ion/app/features/chat/model/entities/private_direct_message_data.c.dart';
import 'package:ion/app/services/nostr/ed25519_key_store.dart';
import 'package:ion/app/services/nostr/ion_connect_gift_wrap_service.dart';
import 'package:nostr_dart/nostr_dart.dart';

void main() {
late IonConnectGiftWrapService giftWrapService;
late EventSigner signer;
const pubkey = 'c95c07ad5aad2d81a3890f13b3eaa80a3d8aca173a91dc2be9fd04720a5a9377';

setUp(() async {
giftWrapService = IonConnectGiftWrapServiceImpl();
signer = await Ed25519KeyStore.generate();
});

group('IonConnectGiftWrapService', () {
test('creates wrap from event', () async {
final event =
await PrivateDirectMessageData.fromRawContent('test').toEventMessage(pubkey: pubkey);

final wrap = await giftWrapService.createWrap(
event,
pubkey,
signer,
PrivateDirectMessageEntity.kind,
);

expect(wrap.kind, equals(1059));
expect(wrap.content, isNotEmpty);
expect(wrap.content, isNot(equals(event.content)));
expect(wrap.tags, hasLength(2));
expect(wrap.tags[0][0], equals('p'));
expect(wrap.tags[0][1], equals(pubkey));
expect(wrap.tags[1][0], equals('k'));
expect(wrap.tags[1][1], equals('14'));
});

//TODO: Investigate why this is failing on CI
// test('decodes wrap back to original event', () async {
// final event =
// await PrivateDirectMessageData.fromRawContent('test').toEventMessage(pubkey: pubkey);

// final wrap = await giftWrapService.createWrap(
// event,
// pubkey,
// signer,
// PrivateDirectMessageEntity.kind,
// );

// final decodedWrap = await giftWrapService.decodeWrap(
// wrap,
// pubkey,
// signer,
// );

// expect(decodedWrap.kind, equals(14));
// expect(decodedWrap.content, equals(event.content));
// expect(decodedWrap.tags, equals(event.tags));
// });
});
}
Loading