Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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
1 change: 1 addition & 0 deletions catalyst_voices/justfile
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ setup-code:
generate-code: setup-code
melos l10n
melos build_runner
melos build_runner_repository
just generate-gateway-services

# Syntax sugar for linking packages and building generated code
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import 'dart:typed_data';

import 'package:catalyst_compression/catalyst_compression.dart';
import 'package:catalyst_cose/catalyst_cose.dart';
import 'package:catalyst_key_derivation/catalyst_key_derivation.dart';
import 'package:cbor/cbor.dart';
import 'package:equatable/equatable.dart';

part 'document_manager_impl.dart';

/// Parses the document from the bytes obtained from [Document.toBytes].
///
/// Usually this would convert the [bytes] into a [String],
/// decode a [String] into a json and then parse the data class
/// from the json representation.
typedef DocumentParser<T extends Document> = T Function(Uint8List bytes);

/// Manages the [SignedDocument]s.
abstract interface class DocumentManager {
/// The default constructor for the [DocumentManager],
/// provides the default implementation of the interface.
const factory DocumentManager() = _DocumentManagerImpl;

/// Parses the document from the [bytes] representation.
///
/// The [parser] must be able to parse the document
/// from the bytes produced by [Document.toBytes].
///
/// The implementation of this method must be able to understand the [bytes]
/// that are obtained from the [SignedDocument.toBytes] method.
Future<SignedDocument<T>> parseDocument<T extends Document>(
Uint8List bytes, {
required DocumentParser<T> parser,
});

/// Signs the [document] with a single [privateKey].
///
/// The [publicKey] will be added as metadata in the signed document
/// so that it's easier to identify who signed it.
Future<SignedDocument<T>> signDocument<T extends Document>(
T document, {
required Uint8List publicKey,
required Uint8List privateKey,
});
}

/// Represents an abstract document that is protected
/// with cryptographic signature.
///
/// The [document] payload can be UTF-8 encoded bytes, a binary data
/// or anything else that can be represented in binary format.
abstract interface class SignedDocument<T extends Document> extends Equatable {
/// The default constructor for the [SignedDocument].
const SignedDocument();

/// A getter that returns a parsed document.
T get document;

/// Verifies if the [document] has been signed by a private key
/// that belongs to the given [publicKey].
Future<bool> verifySignature(Uint8List publicKey);

/// Converts the document into binary representation.
Uint8List toBytes();
}

/// Represents an abstract document that can be represented in binary format.
// ignore: one_member_abstracts
abstract interface class Document {
/// Converts the document into a binary representation.
///
/// See [DocumentParser].
Uint8List toBytes();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
part of 'document_manager.dart';

const _brotliEncoding = StringValue(CoseValues.brotliContentEncoding);

final class _DocumentManagerImpl implements DocumentManager {
const _DocumentManagerImpl();

@override
Future<SignedDocument<T>> parseDocument<T extends Document>(
Uint8List bytes, {
required DocumentParser<T> parser,
}) async {
final coseSign = CoseSign.fromCbor(cbor.decode(bytes));
final payload = await _brotliDecompressPayload(coseSign);
final document = parser(payload);
return _CoseSignedDocument(coseSign, document);
}

@override
Future<SignedDocument<T>> signDocument<T extends Document>(
T document, {
required Uint8List publicKey,
required Uint8List privateKey,
}) async {
final compressedPayload = await _brotliCompressPayload(document.toBytes());

final coseSign = await CoseSign.sign(
protectedHeaders: const CoseHeaders.protected(
contentEncoding: _brotliEncoding,
),
unprotectedHeaders: const CoseHeaders.unprotected(),
payload: compressedPayload,
signers: [_Bip32Ed25519XSigner(publicKey, privateKey)],
);

return _CoseSignedDocument(coseSign, document);
}

Future<Uint8List> _brotliCompressPayload(Uint8List payload) async {
final compressor = CatalystCompression.instance.brotli;
final compressed = await compressor.compress(payload);
return Uint8List.fromList(compressed);
}

Future<Uint8List> _brotliDecompressPayload(CoseSign coseSign) async {
if (coseSign.protectedHeaders.contentEncoding == _brotliEncoding) {
final compressor = CatalystCompression.instance.brotli;
final decompressed = await compressor.decompress(coseSign.payload);
return Uint8List.fromList(decompressed);
} else {
return coseSign.payload;
}
}
}

final class _CoseSignedDocument<T extends Document> extends SignedDocument<T> {
final CoseSign _coseSign;
final T _document;

const _CoseSignedDocument(this._coseSign, this._document);

@override
T get document => _document;

@override
Future<bool> verifySignature(Uint8List publicKey) async {
return _coseSign.verify(
verifier: _Bip32Ed25519XVerifier(publicKey),
);
}

@override
Uint8List toBytes() {
final bytes = cbor.encode(_coseSign.toCbor());
return Uint8List.fromList(bytes);
}

@override
List<Object?> get props => [_coseSign, _document];
}

final class _Bip32Ed25519XSigner implements CatalystCoseSigner {
final Uint8List publicKey;
final Uint8List privateKey;

const _Bip32Ed25519XSigner(this.publicKey, this.privateKey);
@override
StringOrInt? get alg => const IntValue(CoseValues.eddsaAlg);

@override
Future<Uint8List?> get kid async => publicKey;

@override
Future<Uint8List> sign(Uint8List data) async {
final pk = Bip32Ed25519XPrivateKeyFactory.instance.fromBytes(privateKey);
final signature = await pk.sign(data);
return Uint8List.fromList(signature.bytes);
}
}

final class _Bip32Ed25519XVerifier implements CatalystCoseVerifier {
final Uint8List publicKey;

const _Bip32Ed25519XVerifier(this.publicKey);

@override
Future<Uint8List?> get kid async => publicKey;

@override
Future<bool> verify(Uint8List data, Uint8List signature) async {
final pk = Bip32Ed25519XPublicKeyFactory.instance.fromBytes(publicKey);
return pk.verify(
data,
signature: Bip32Ed25519XSignatureFactory.instance.fromBytes(signature),
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,16 @@ environment:

dependencies:
catalyst_cardano_serialization: ^0.4.0
catalyst_compression: ^0.3.0
catalyst_cose: ^0.3.0
catalyst_key_derivation: ^0.1.0
catalyst_voices_models:
path: ../catalyst_voices_models
cbor: ^6.2.0
collection: ^1.18.0
convert: ^3.1.1
cryptography: ^2.7.0
equatable: ^2.0.7
flutter:
sdk: flutter
flutter_secure_storage: ^9.2.2
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
import 'dart:convert';

import 'package:catalyst_compression/catalyst_compression.dart';
import 'package:catalyst_key_derivation/catalyst_key_derivation.dart';
import 'package:catalyst_voices_shared/src/document/document_manager.dart';
import 'package:convert/convert.dart';
import 'package:equatable/equatable.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_test/flutter_test.dart';

final _privateKey = Uint8List.fromList(List.filled(32, 0));
final _publicKey = Uint8List.fromList(List.filled(32, 1));
final _signature = Uint8List.fromList(List.filled(32, 2));

void main() {
group(DocumentManager, () {
const documentManager = DocumentManager();

setUpAll(() {
CatalystCompressionPlatform.instance = _FakeCatalystCompressionPlatform();

Bip32Ed25519XPublicKeyFactory.instance =
_FakeBip32Ed25519XPublicKeyFactory();

Bip32Ed25519XPrivateKeyFactory.instance =
_FakeBip32Ed25519XPrivateKeyFactory();

Bip32Ed25519XSignatureFactory.instance =
_FakeBip32Ed25519XSignatureFactory();
});

test(
'signDocument creates a signed document '
'that can be converted from/to bytes', () async {
const document = _JsonDocument('title');

final signedDocument = await documentManager.signDocument(
document,
publicKey: _publicKey,
privateKey: _privateKey,
);

expect(signedDocument.document, equals(document));

final isVerified = await signedDocument.verifySignature(_publicKey);
expect(isVerified, isTrue);

final signedDocumentBytes = signedDocument.toBytes();
final parsedDocument = await documentManager.parseDocument(
signedDocumentBytes,
parser: _JsonDocument.fromBytes,
);

expect(parsedDocument, equals(signedDocument));
});
});
}

final class _JsonDocument extends Equatable implements Document {
final String title;

const _JsonDocument(this.title);

factory _JsonDocument.fromBytes(Uint8List bytes) {
final string = utf8.decode(bytes);
final map = json.decode(string);
return _JsonDocument.fromJson(map as Map<String, dynamic>);
}

factory _JsonDocument.fromJson(Map<String, dynamic> map) {
return _JsonDocument(map['title'] as String);
}

Map<String, dynamic> toJson() {
return {'title': title};
}

@override
Uint8List toBytes() {
final jsonString = json.encode(toJson());
return utf8.encode(jsonString);
}

@override
List<Object?> get props => [title];
}

class _FakeCatalystCompressionPlatform extends CatalystCompressionPlatform {
@override
CatalystCompressor get brotli => const _FakeCompressor();
}

final class _FakeCompressor implements CatalystCompressor {
const _FakeCompressor();

@override
Future<List<int>> compress(List<int> bytes) async => bytes;

@override
Future<List<int>> decompress(List<int> bytes) async => bytes;
}

class _FakeBip32Ed25519XPublicKeyFactory extends Bip32Ed25519XPublicKeyFactory {
@override
Bip32Ed25519XPublicKey fromBytes(List<int> bytes) {
return _FakeBip32Ed22519XPublicKey(bytes: bytes);
}
}

class _FakeBip32Ed25519XPrivateKeyFactory
extends Bip32Ed25519XPrivateKeyFactory {
@override
Bip32Ed25519XPrivateKey fromBytes(List<int> bytes) {
return _FakeBip32Ed22519XPrivateKey(bytes: bytes);
}
}

class _FakeBip32Ed25519XSignatureFactory extends Bip32Ed25519XSignatureFactory {
@override
Bip32Ed25519XSignature fromBytes(List<int> bytes) {
return _FakeBip32Ed22519XSignature(bytes: bytes);
}
}

class _FakeBip32Ed22519XPublicKey extends Fake
implements Bip32Ed25519XPublicKey {
@override
final List<int> bytes;

_FakeBip32Ed22519XPublicKey({required this.bytes});

@override
Future<bool> verify(
List<int> message, {
required Bip32Ed25519XSignature signature,
}) async {
return listEquals(signature.bytes, _signature);
}

@override
String toHex() => hex.encode(bytes);
}

class _FakeBip32Ed22519XPrivateKey extends Fake
implements Bip32Ed25519XPrivateKey {
@override
final List<int> bytes;

_FakeBip32Ed22519XPrivateKey({required this.bytes});

@override
Future<Bip32Ed25519XSignature> sign(List<int> message) async {
return _FakeBip32Ed22519XSignature(bytes: _signature);
}

@override
String toHex() => hex.encode(bytes);
}

class _FakeBip32Ed22519XSignature extends Fake
implements Bip32Ed25519XSignature {
@override
final List<int> bytes;

_FakeBip32Ed22519XSignature({required this.bytes});

@override
String toHex() => hex.encode(bytes);
}
Loading
Loading