Skip to content

Commit

Permalink
add replay protection to core encode/decode
Browse files Browse the repository at this point in the history
  • Loading branch information
quetool committed Sep 28, 2023
1 parent bdda5e9 commit 0ec9852
Show file tree
Hide file tree
Showing 10 changed files with 92 additions and 57 deletions.
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

0 comments on commit 0ec9852

Please sign in to comment.