diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..effe43c --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,3 @@ +## 1.0.0 + +- Initial version. diff --git a/analysis_options.yaml b/analysis_options.yaml new file mode 100644 index 0000000..dee8927 --- /dev/null +++ b/analysis_options.yaml @@ -0,0 +1,30 @@ +# This file configures the static analysis results for your project (errors, +# warnings, and lints). +# +# This enables the 'recommended' set of lints from `package:lints`. +# This set helps identify many issues that may lead to problems when running +# or consuming Dart code, and enforces writing Dart using a single, idiomatic +# style and format. +# +# If you want a smaller set of lints you can change this to specify +# 'package:lints/core.yaml'. These are just the most critical lints +# (the recommended set includes the core lints). +# The core lints are also what is used by pub.dev for scoring packages. + +include: package:lints/recommended.yaml + +# Uncomment the following section to specify additional rules. + +# linter: +# rules: +# - camel_case_types + +# analyzer: +# exclude: +# - path/to/excluded/files/** + +# For more information about the core and recommended set of lints, see +# https://dart.dev/go/core-lints + +# For additional information about configuring this file, see +# https://dart.dev/guides/language/analysis-options diff --git a/bin/torchat_node.dart b/bin/torchat_node.dart new file mode 100644 index 0000000..4c331b4 --- /dev/null +++ b/bin/torchat_node.dart @@ -0,0 +1,7 @@ +import 'package:torchat/core_api/torchat_service.dart'; + +Future main(List args) async { + final service = TorChatService(); + await service.startNode(); // Boots Tor, gRPC server, discovery loop + print('TorChat node running. Press Ctrl-C to exit.'); +} \ No newline at end of file diff --git a/bin/torchat_rest_api.dart b/bin/torchat_rest_api.dart new file mode 100644 index 0000000..e69de29 diff --git a/lib/core_api/message_handler.dart b/lib/core_api/message_handler.dart new file mode 100644 index 0000000..e69de29 diff --git a/lib/core_api/torchat_server.dart b/lib/core_api/torchat_server.dart new file mode 100644 index 0000000..e69de29 diff --git a/lib/core_api/torchat_service.dart b/lib/core_api/torchat_service.dart new file mode 100644 index 0000000..6b21353 --- /dev/null +++ b/lib/core_api/torchat_service.dart @@ -0,0 +1,25 @@ +import 'package:torchat/src/p2p/grpc/server/grpc_server.dart'; +import 'package:torchat/src/p2p/message_queue/outgoing_queue.dart'; +import 'package:torchat/src/p2p/sync_manager.dart'; + +class TorChatService { + final OutgoingQueue _queue = OutgoingQueue(); + late final GrpcServerRunner _grpcServer; + late final SyncManager _sync; + + Future startNode() async { + // TODO: start Tor/Arti here and open hidden service + _grpcServer = GrpcServerRunner(); + await _grpcServer.start(_queue); + + _sync = SyncManager(_queue); + _sync.start(); + + // TODO: bootstrap peer discovery loop + } + + Future stopNode() async { + await _grpcServer.stop(); + _sync.stop(); + } +} \ No newline at end of file diff --git a/lib/src/encryption/ed25519_utils.dart b/lib/src/encryption/ed25519_utils.dart new file mode 100644 index 0000000..e69de29 diff --git a/lib/src/encryption/message_seal.dart b/lib/src/encryption/message_seal.dart new file mode 100644 index 0000000..e69de29 diff --git a/lib/src/models/peer.dart b/lib/src/models/peer.dart new file mode 100644 index 0000000..e69de29 diff --git a/lib/src/models/queued_message.dart b/lib/src/models/queued_message.dart new file mode 100644 index 0000000..da7c979 --- /dev/null +++ b/lib/src/models/queued_message.dart @@ -0,0 +1,22 @@ +class QueuedMessage { + final String recipientId; + final String message; + final DateTime timestamp; + final int retryCount; + + QueuedMessage({ + required this.recipientId, + required this.message, + DateTime? timestamp, + this.retryCount = 0, + }) : timestamp = timestamp ?? DateTime.now(); + + QueuedMessage copyWith({int? retryCount}) { + return QueuedMessage( + recipientId: recipientId, + message: message, + timestamp: timestamp, + retryCount: retryCount ?? this.retryCount, + ); + } +} \ No newline at end of file diff --git a/lib/src/models/user.dart b/lib/src/models/user.dart new file mode 100644 index 0000000..e69de29 diff --git a/lib/src/p2p/grpc/client/peer_client.dart b/lib/src/p2p/grpc/client/peer_client.dart new file mode 100644 index 0000000..e69de29 diff --git a/lib/src/p2p/grpc/server/grpc_server.dart b/lib/src/p2p/grpc/server/grpc_server.dart new file mode 100644 index 0000000..35eac53 --- /dev/null +++ b/lib/src/p2p/grpc/server/grpc_server.dart @@ -0,0 +1,36 @@ +import 'dart:io'; +import 'package:grpc/grpc.dart'; +import 'package:torchat/src/p2p/message_queue/outgoing_queue.dart'; + +class TorChatGrpcServer extends ChatServiceBase { + final OutgoingQueue queue; + + TorChatGrpcServer(this.queue); + + // RPC: SendMessage + @override + Future sendMessage( + ServiceCall call, ChatMessageRequest req) async { + // Simply enqueue for now + queue.enqueue(req.message); + return ChatMessageReply(success: true, messageId: req.message.id); + } + + // TODO: implement StreamMessages, FetchHistory, etc. +} + +class GrpcServerRunner { + Server? _server; + + Future start(OutgoingQueue queue, {int port = 20900}) async { + _server = Server( + [TorChatGrpcServer(queue)], + const [], + CodecRegistry(codecs: const [GzipCodec(), IdentityCodec()]), + ); + await _server!.serve(port: port); + stdout.writeln('gRPC server listening on port $port'); + } + + Future stop() => _server?.shutdown() ?? Future.value(); +} \ No newline at end of file diff --git a/lib/src/p2p/message_queue/inbound_handler.dart b/lib/src/p2p/message_queue/inbound_handler.dart new file mode 100644 index 0000000..e69de29 diff --git a/lib/src/p2p/message_queue/outgoing_queue.dart b/lib/src/p2p/message_queue/outgoing_queue.dart new file mode 100644 index 0000000..c121e4f --- /dev/null +++ b/lib/src/p2p/message_queue/outgoing_queue.dart @@ -0,0 +1,14 @@ + + +class OutgoingQueue { + final _list = []; + + void enqueue(Message msg) => _list.add(msg); + bool get hasMessages => _list.isNotEmpty; + + List drain([int max = 50]) { + final drain = _list.take(max).toList(); + _list.removeRange(0, drain.length); + return drain; + } +} \ No newline at end of file diff --git a/lib/src/p2p/sync_manager.dart b/lib/src/p2p/sync_manager.dart new file mode 100644 index 0000000..0114330 --- /dev/null +++ b/lib/src/p2p/sync_manager.dart @@ -0,0 +1,26 @@ +import 'dart:async'; + +import 'package:torchat/src/p2p/message_queue/outgoing_queue.dart'; + +class SyncManager { + final OutgoingQueue queue; + final Duration interval; + Timer? _timer; + + SyncManager(this.queue, {this.interval = const Duration(seconds: 10)}); + + void start() { + _timer ??= Timer.periodic(interval, (_) => _flush()); + } + + void stop() => _timer?.cancel(); + + Future _flush() async { + if (!queue.hasMessages) return; + final msgs = queue.drain(); + // TODO: iterate peers from discovery service and send via gRPC client + for (final msg in msgs) { + print('[SyncManager] Would send message ${msg.id}'); + } + } +} \ No newline at end of file diff --git a/lib/src/p2p/tor/tor_service.dart b/lib/src/p2p/tor/tor_service.dart new file mode 100644 index 0000000..e69de29 diff --git a/lib/src/pow/pow_verifier.dart b/lib/src/pow/pow_verifier.dart new file mode 100644 index 0000000..e69de29 diff --git a/lib/src/relay.dart b/lib/src/relay.dart new file mode 100644 index 0000000..e69de29 diff --git a/lib/src/repository/group_repository.dart b/lib/src/repository/group_repository.dart new file mode 100644 index 0000000..e69de29 diff --git a/lib/src/repository/message_repository.dart b/lib/src/repository/message_repository.dart new file mode 100644 index 0000000..e69de29 diff --git a/lib/src/repository/user_repository.dart b/lib/src/repository/user_repository.dart new file mode 100644 index 0000000..e69de29 diff --git a/lib/src/storage/spool.dart b/lib/src/storage/spool.dart new file mode 100644 index 0000000..e69de29 diff --git a/lib/src/storage/sqlite/migrations/v1.sql b/lib/src/storage/sqlite/migrations/v1.sql new file mode 100644 index 0000000..e69de29 diff --git a/lib/torchat.dart b/lib/torchat.dart new file mode 100644 index 0000000..f64ad72 --- /dev/null +++ b/lib/torchat.dart @@ -0,0 +1,3 @@ +int calculate() { + return 6 * 7; +} diff --git a/protospec/chat.proto b/protospec/chat.proto new file mode 100644 index 0000000..a7ff4f9 --- /dev/null +++ b/protospec/chat.proto @@ -0,0 +1,75 @@ +syntax = "proto3"; + +package torchat; + +import "user.proto"; + +option java_package = "org.torchat.proto"; +option java_multiple_files = true; + +message GroupId { + string value = 1; +} + +message GroupProfile { + GroupId group_id = 1; + string display_name = 2; + string description = 3; + repeated user.UserId members = 4; + user.UserId creator = 5; + int64 created_at = 6; + string avatar_url = 7; + repeated string tags = 8; + string rules = 9; +} + +message GroupMember { + user.UserId user_id = 1; + bool is_admin = 2; + bool is_muted = 3; + int64 joined_at = 4; +} + +message GroupMessage { + GroupId group_id = 1; + user.UserId sender = 2; + string content = 3; + int64 timestamp = 4; + string message_id = 5; +} + +message CreateGroupRequest { + GroupProfile profile = 1; +} + +message CreateGroupReply { + GroupId group_id = 1; +} + +message GetGroupRequest { + GroupId group_id = 1; +} + +message GetGroupReply { + GroupProfile profile = 1; + repeated GroupMember members = 2; +} + +message SendGroupMessageRequest { + GroupMessage message = 1; +} + +message SendGroupMessageReply { + bool success = 1; +} + +message StreamGroupMessagesRequest { + GroupId group_id = 1; +} + +service GroupService { + rpc CreateGroup(CreateGroupRequest) returns (CreateGroupReply); + rpc GetGroup(GetGroupRequest) returns (GetGroupReply); + rpc SendMessage(SendGroupMessageRequest) returns (SendGroupMessageReply); + rpc StreamMessages(StreamGroupMessagesRequest) returns (stream GroupMessage); +} diff --git a/protospec/group.proto b/protospec/group.proto new file mode 100644 index 0000000..a7ff4f9 --- /dev/null +++ b/protospec/group.proto @@ -0,0 +1,75 @@ +syntax = "proto3"; + +package torchat; + +import "user.proto"; + +option java_package = "org.torchat.proto"; +option java_multiple_files = true; + +message GroupId { + string value = 1; +} + +message GroupProfile { + GroupId group_id = 1; + string display_name = 2; + string description = 3; + repeated user.UserId members = 4; + user.UserId creator = 5; + int64 created_at = 6; + string avatar_url = 7; + repeated string tags = 8; + string rules = 9; +} + +message GroupMember { + user.UserId user_id = 1; + bool is_admin = 2; + bool is_muted = 3; + int64 joined_at = 4; +} + +message GroupMessage { + GroupId group_id = 1; + user.UserId sender = 2; + string content = 3; + int64 timestamp = 4; + string message_id = 5; +} + +message CreateGroupRequest { + GroupProfile profile = 1; +} + +message CreateGroupReply { + GroupId group_id = 1; +} + +message GetGroupRequest { + GroupId group_id = 1; +} + +message GetGroupReply { + GroupProfile profile = 1; + repeated GroupMember members = 2; +} + +message SendGroupMessageRequest { + GroupMessage message = 1; +} + +message SendGroupMessageReply { + bool success = 1; +} + +message StreamGroupMessagesRequest { + GroupId group_id = 1; +} + +service GroupService { + rpc CreateGroup(CreateGroupRequest) returns (CreateGroupReply); + rpc GetGroup(GetGroupRequest) returns (GetGroupReply); + rpc SendMessage(SendGroupMessageRequest) returns (SendGroupMessageReply); + rpc StreamMessages(StreamGroupMessagesRequest) returns (stream GroupMessage); +} diff --git a/protospec/user.proto b/protospec/user.proto new file mode 100644 index 0000000..53f226c --- /dev/null +++ b/protospec/user.proto @@ -0,0 +1,100 @@ +syntax = "proto3"; + +// --------------------------------------------------------------------------- +// TorChat User Identity & Reputation Spec (v0.1) +// --------------------------------------------------------------------------- +// * A user is a long-term Ed25519 key-pair. +// * Profile fields are optional and fully signed. +// * Multi-device keys are supported via DeviceKey. +// * Third-party attestations ("signatures") build a web-of-trust layer. +// --------------------------------------------------------------------------- + +package torchat.identity; + +option go_package = "github.com/torchat/proto/identity"; +option java_package = "org.torchat.identity"; +option java_multiple_files = true; + +// ------------------------------------------------- +// Primitive types +// ------------------------------------------------- +message PublicKey { bytes value = 1; } // Ed25519 32-byte +message Signature { bytes value = 1; } // Ed25519 64-byte on SHA-256 hash +message Sha256Hash { bytes value = 1; } // 32-byte hash + +// ------------------------------------------------- +// User Profile (self-signed) +// ------------------------------------------------- +message UserProfile { + PublicKey pubkey = 1; // unique master key + string nickname = 2; // optional alias + string bio = 3; // optional free-text + string avatar_url = 4; // optional (ipfs:// or https://) + int64 created_at = 5; // epoch millis + uint32 version = 6; // profile schema version +} + +// ------------------------------------------------- +// Device key (per-device subkey, signed by master) +// ------------------------------------------------- +message DeviceKey { + string device_id = 1; // random UUID / human name + PublicKey device_pk = 2; // Ed25519 key for this device + int64 created_at = 3; + Signature master_sig = 4; // master key sig over hash(device) +} + +// ------------------------------------------------- +// Third-party attestation (web-of-trust) +// ------------------------------------------------- +message Attestation { + enum Purpose { + GENERIC_TRUST = 0; // default "I trust this user" + MODERATOR_ROLE = 1; + NOTARY_ROLE = 2; // can co-sign trades, escrow etc. + } + + PublicKey signer_pk = 1; // who signs (must be known peer) + bytes subject_pk = 2; // user being attested + Purpose purpose = 3; + string memo = 4; // free text or JSON + int64 timestamp = 5; + Signature signature = 6; // sig(signer) over hash(all above fields) +} + +// ------------------------------------------------- +// Full signed user record +// ------------------------------------------------- +message SignedUser { + UserProfile profile = 1; // MUST contain self-sig + Signature self_sig = 2; // sig(master) over hash(profile) + repeated DeviceKey devices = 3; // 0+ devices + repeated Attestation attestations = 4; // 0+ third-party sigs +} + +// ------------------------------------------------- +// gRPC Identity Service +// ------------------------------------------------- +service IdentityService { + // Register or update your profile. Must include self_sig. + rpc RegisterUser (SignedUser) returns (Ack); + + // Add or revoke a device key (master-signed). + rpc UpsertDeviceKey (DeviceKey) returns (Ack); + + // Add a third-party attestation. + rpc AddAttestation (Attestation) returns (Ack); + + // Get full user record. + rpc GetUser (PublicKey) returns (SignedUser); + + // Stream attestations about a user. + rpc StreamAttestations(PublicKey) returns (stream Attestation); +} + +// Generic acknowledge wrapper +message Ack { + enum Status { OK = 0; ERROR = 1; } + Status status = 1; + string message = 2; // optional error / info +} diff --git a/pubspec.yaml b/pubspec.yaml new file mode 100644 index 0000000..a9632a4 --- /dev/null +++ b/pubspec.yaml @@ -0,0 +1,21 @@ +name: torchat +description: A gRPC-based anonymous chat node (client / server) built with Dart and Tor/Arti +version: 0.1.0 +homepage: https://foss.haveno.com/tor-project/torchat +environment: + sdk: '>=3.4.0 <4.0.0' + +dependencies: + grpc: ^3.2.4 + protobuf: ^3.1.0 + convert: ^3.1.1 + logging: ^1.2.0 + args: ^2.5.0 + yaml: ^3.1.2 + path: ^1.9.0 + +dev_dependencies: + lints: ^3.0.0 + test: ^1.25.0 + build_runner: ^2.4.7 + protoc_plugin: ^21.1.2 \ No newline at end of file diff --git a/test/torchat_test.dart b/test/torchat_test.dart new file mode 100644 index 0000000..ec7c2d0 --- /dev/null +++ b/test/torchat_test.dart @@ -0,0 +1,8 @@ +import 'package:torchat/torchat.dart'; +import 'package:test/test.dart'; + +void main() { + test('calculate', () { + expect(calculate(), 42); + }); +}