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

Add replay protection to core encode/decode #196

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
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 example/dapp/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ class _MyHomePageState extends State<MyHomePage> {

Future<void> initialize() async {
// try {
print('Project ID: ${DartDefines.projectId}');
debugPrint('Project ID: ${DartDefines.projectId}');
_web3App = await Web3App.createInstance(
projectId: DartDefines.projectId,
metadata: const PairingMetadata(
Expand Down
4 changes: 2 additions & 2 deletions example/wallet/ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -104,8 +104,8 @@ SPEC CHECKSUMS:
mobile_scanner: 47056db0c04027ea5f41a716385542da28574662
nanopb: b552cce312b6c8484180ef47159bc0f65a1f0431
PromisesObjC: 09985d6d70fbe7878040aa746d78236e6946d2ef
shared_preferences_foundation: e2dae3258e06f44cc55f49d42024fd8dd03c590c
shared_preferences_foundation: 5b919d13b803cadd15ed2dc053125c68730e5126

PODFILE CHECKSUM: ef19549a9bc3046e7bb7d2fab4d021637c0c58a3

COCOAPODS: 1.11.3
COCOAPODS: 1.13.0
9 changes: 4 additions & 5 deletions example/wallet/ios/Runner.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,6 @@
E244D63034062C498A453D0D /* Pods-Runner.release.xcconfig */,
36DDA0CA204E24EC1CDA9FAA /* Pods-Runner.profile.xcconfig */,
);
name = Pods;
path = Pods;
sourceTree = "<group>";
};
Expand Down Expand Up @@ -156,7 +155,7 @@
97C146E61CF9000F007C117D /* Project object */ = {
isa = PBXProject;
attributes = {
LastUpgradeCheck = 1300;
LastUpgradeCheck = 1430;
ORGANIZATIONNAME = "";
TargetAttributes = {
97C146ED1CF9000F007C117D = {
Expand Down Expand Up @@ -359,7 +358,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = 624CBRZYXF;
DEVELOPMENT_TEAM = W5R8AG9K22;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
Expand Down Expand Up @@ -488,7 +487,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = 624CBRZYXF;
DEVELOPMENT_TEAM = W5R8AG9K22;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
Expand All @@ -511,7 +510,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = 624CBRZYXF;
DEVELOPMENT_TEAM = W5R8AG9K22;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1300"
LastUpgradeVersion = "1430"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
Expand Down
60 changes: 43 additions & 17 deletions lib/apis/core/crypto/crypto.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import 'dart:convert';
import 'dart:typed_data';

import 'package:convert/convert.dart';
import 'package:flutter/foundation.dart';
import 'package:walletconnect_flutter_v2/apis/core/crypto/crypto_models.dart';
import 'package:walletconnect_flutter_v2/apis/core/crypto/crypto_utils.dart';
import 'package:walletconnect_flutter_v2/apis/core/i_core.dart';
Expand Down Expand Up @@ -132,20 +133,23 @@ class Crypto implements ICrypto {
);
}

final String message = jsonEncode(payload);

if (utils.isTypeOneEnvelope(params)) {
final String selfPublicKey = params.senderPublicKey!;
final String peerPublicKey = params.receiverPublicKey!;
final selfPublicKey = params.senderPublicKey!;
final peerPublicKey = params.receiverPublicKey!;
topic = await generateSharedKey(selfPublicKey, peerPublicKey);
}

final String? symKey = _getSymKey(topic);
final protectedPayload = Map.fromEntries(
[MapEntry('topic', topic), ...payload.entries],
);
final message = jsonEncode(protectedPayload);

final symKey = _getSymKey(topic);
if (symKey == null) {
return null;
}

final String result = await utils.encrypt(
final result = await utils.encrypt(
message,
symKey,
type: params.type,
Expand All @@ -156,31 +160,53 @@ class Crypto implements ICrypto {
}

@override
Future<String?> decode(
Future<Map<String, dynamic>?> decode(
String topic,
String encoded, {
DecodeOptions? options,
}) async {
_checkInitialized();

final EncodingValidation params = utils.validateDecoding(
final params = utils.validateDecoding(
encoded,
receiverPublicKey: options?.receiverPublicKey,
);

if (utils.isTypeOneEnvelope(params)) {
final String selfPublicKey = params.receiverPublicKey!;
final String peerPublicKey = params.senderPublicKey!;
final selfPublicKey = params.receiverPublicKey!;
final peerPublicKey = params.senderPublicKey!;
topic = await generateSharedKey(selfPublicKey, peerPublicKey);
}
final String? symKey = _getSymKey(topic);
if (symKey == null) {
return null;
}

final String message = await utils.decrypt(symKey, encoded);

return message;
try {
final symKey = _getSymKey(topic);
if (symKey == null) {
return null;
}

final message = await utils.decrypt(symKey, encoded);

final payload = jsonDecode(message) as Map<String, dynamic>;
if (payload.containsKey('topic')) {
final payloadTopic = payload['topic'] as String;
if (payloadTopic != topic) {
throw Errors.getInternalError(
Errors.MISMATCHED_TOPIC,
context: 'decode() Mismatched topic decoded from message.',
);
}
payload.remove('topic');
}

return payload;
} catch (e) {
throw Errors.getInternalError(
Errors.PARSING_FAILED,
context: 'Failed to decode message from topic: $topic, '
'clientId: ${await getClientId()} '
'Exception: $e',
);
}
}

@override
Expand Down
2 changes: 1 addition & 1 deletion lib/apis/core/crypto/i_crypto.dart
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ abstract class ICrypto {
Map<String, dynamic> payload, {
EncodeOptions? options,
});
Future<String?> decode(
Future<Map<String, dynamic>?> decode(
String topic,
String encoded, {
DecodeOptions? options,
Expand Down
15 changes: 6 additions & 9 deletions lib/apis/core/pairing/pairing.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import 'dart:async';
import 'dart:convert';

import 'package:event/event.dart';
import 'package:walletconnect_flutter_v2/apis/core/pairing/i_json_rpc_history.dart';
Expand Down Expand Up @@ -630,26 +629,24 @@ class Pairing implements IPairing {
}

// Decode the message
String? payloadString = await core.crypto.decode(
Map<String, dynamic>? payload = await core.crypto.decode(
event.topic,
event.message,
options: DecodeOptions(
receiverPublicKey: receiverPublicKey?.publicKey,
),
);

if (payloadString == null) {
if (payload == null) {
return;
}
// print(payloadString);

Map<String, dynamic> data = jsonDecode(payloadString);
core.logger.i('Pairing _onMessageEvent, Received data: $data');
core.logger.i('Pairing _onMessageEvent, Received data: $payload');

// If it's an rpc request, handle it
// print('Pairing: Received data: $data');
if (data.containsKey('method')) {
final request = JsonRpcRequest.fromJson(data);
if (payload.containsKey('method')) {
final request = JsonRpcRequest.fromJson(payload);
if (routerMapRequest.containsKey(request.method)) {
routerMapRequest[request.method]!.function(event.topic, request);
} else {
Expand All @@ -658,7 +655,7 @@ class Pairing implements IPairing {
}
// Otherwise handle it as a response
else {
final response = JsonRpcResponse.fromJson(data);
final response = JsonRpcResponse.fromJson(payload);

// Only handle the response if we have a record of the request
// final JsonRpcRecord? record = history.get(response.id.toString());
Expand Down
5 changes: 5 additions & 0 deletions lib/apis/utils/errors.dart
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,7 @@ class Errors {
static const UNKNOWN_TYPE = 'UNKNOWN_TYPE';
static const MISMATCHED_TOPIC = 'MISMATCHED_TOPIC';
static const NON_CONFORMING_NAMESPACES = 'NON_CONFORMING_NAMESPACES';
static const PARSING_FAILED = 'PARSING_FAILED';

static const INTERNAL_ERRORS = {
NOT_INITIALIZED: {
Expand Down Expand Up @@ -225,6 +226,10 @@ class Errors {
'message': 'Non conforming namespaces.',
'code': 9,
},
PARSING_FAILED: {
'message': 'Failed to decode/encode.',
'code': 10,
},
};

static WalletConnectError getInternalError(
Expand Down
12 changes: 6 additions & 6 deletions test/core_api/crypto_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -234,22 +234,22 @@ void main() {
final topic = await cryptoAPI.setSymKey(SYM_KEY);
final String? encoded = await cryptoAPI.encode(topic, PAYLOAD);

final String? decoded = await cryptoAPI.decode(topic, encoded!);
expect(decoded, jsonEncode(PAYLOAD));
final decoded = await cryptoAPI.decode(topic, encoded!);
expect(jsonEncode(decoded), jsonEncode(PAYLOAD));

final String? decoded2 = await cryptoAPI.decode(topic, ENCODED);
expect(decoded2, jsonEncode(PAYLOAD));
final decoded2 = await cryptoAPI.decode(topic, ENCODED);
expect(jsonEncode(decoded2), jsonEncode(PAYLOAD));
},
);

test(
'returns null if the passed topic is known',
() async {
final topic = CryptoUtils().hashKey(SYM_KEY);
final String? encoded = await cryptoAPI.encode(topic, PAYLOAD);
final encoded = await cryptoAPI.encode(topic, PAYLOAD);
expect(encoded, isNull);

final String? decoded = await cryptoAPI.decode(topic, ENCODED);
final decoded = await cryptoAPI.decode(topic, ENCODED);
expect(decoded, isNull);
},
);
Expand Down
38 changes: 23 additions & 15 deletions test/shared/shared_test_utils.mocks.dart
Original file line number Diff line number Diff line change
Expand Up @@ -584,7 +584,7 @@ class MockCrypto extends _i1.Mock implements _i19.Crypto {
returnValue: _i18.Future<String?>.value(),
) as _i18.Future<String?>);
@override
_i18.Future<String?> decode(
_i18.Future<Map<String, dynamic>?> decode(
String? topic,
String? encoded, {
_i2.DecodeOptions? options,
Expand All @@ -598,8 +598,8 @@ class MockCrypto extends _i1.Mock implements _i19.Crypto {
],
{#options: options},
),
returnValue: _i18.Future<String?>.value(),
) as _i18.Future<String?>);
returnValue: _i18.Future<Map<String, dynamic>?>.value(),
) as _i18.Future<Map<String, dynamic>?>);
@override
_i18.Future<String> signJWT(String? aud) => (super.noSuchMethod(
Invocation.method(
Expand Down Expand Up @@ -924,6 +924,19 @@ class MockCore extends _i1.Mock implements _i23.Core {
_i1.throwOnMissingStub(this);
}

@override
String get relayUrl => (super.noSuchMethod(
Invocation.getter(#relayUrl),
returnValue: '',
) as String);
@override
set relayUrl(String? _relayUrl) => super.noSuchMethod(
Invocation.setter(
#relayUrl,
_relayUrl,
),
returnValueForMissingStub: null,
);
@override
String get projectId => (super.noSuchMethod(
Invocation.getter(#projectId),
Expand Down Expand Up @@ -1015,14 +1028,6 @@ class MockCore extends _i1.Mock implements _i23.Core {
returnValueForMissingStub: null,
);
@override
_i15.Logger get logger => (super.noSuchMethod(
Invocation.getter(#logger),
returnValue: _FakeLogger_15(
this,
Invocation.getter(#logger),
),
) as _i15.Logger);
@override
_i7.IStore<Map<String, dynamic>> get storage => (super.noSuchMethod(
Invocation.getter(#storage),
returnValue: _FakeIStore_7<Map<String, dynamic>>(
Expand All @@ -1049,10 +1054,13 @@ class MockCore extends _i1.Mock implements _i23.Core {
returnValue: '',
) as String);
@override
String get relayUrl => (super.noSuchMethod(
Invocation.getter(#relayUrl),
returnValue: '',
) as String);
_i15.Logger get logger => (super.noSuchMethod(
Invocation.getter(#logger),
returnValue: _FakeLogger_15(
this,
Invocation.getter(#logger),
),
) as _i15.Logger);
@override
_i18.Future<void> start() => (super.noSuchMethod(
Invocation.method(
Expand Down