diff --git a/lib/encryption/cross_signing.dart b/lib/encryption/cross_signing.dart index 6d243f21..60d9a64c 100644 --- a/lib/encryption/cross_signing.dart +++ b/lib/encryption/cross_signing.dart @@ -16,9 +16,9 @@ * along with this program. If not, see . */ -import 'dart:convert'; import 'dart:typed_data'; +import 'package:matrix/encryption/utils/base64_unpadded.dart'; import 'package:olm/olm.dart' as olm; import '../matrix.dart'; @@ -33,7 +33,7 @@ class CrossSigning { (String secret) async { final keyObj = olm.PkSigning(); try { - return keyObj.init_with_seed(base64.decode(secret)) == + return keyObj.init_with_seed(base64decodeUnpadded(secret)) == client.userDeviceKeys[client.userID]!.selfSigningKey!.ed25519Key; } catch (_) { return false; @@ -45,7 +45,7 @@ class CrossSigning { (String secret) async { final keyObj = olm.PkSigning(); try { - return keyObj.init_with_seed(base64.decode(secret)) == + return keyObj.init_with_seed(base64decodeUnpadded(secret)) == client.userDeviceKeys[client.userID]!.userSigningKey!.ed25519Key; } catch (_) { return false; @@ -87,8 +87,8 @@ class CrossSigning { ); await handle.maybeCacheAll(); } - final masterPrivateKey = - base64.decode(await handle.getStored(EventTypes.CrossSigningMasterKey)); + final masterPrivateKey = base64decodeUnpadded( + await handle.getStored(EventTypes.CrossSigningMasterKey)); final keyObj = olm.PkSigning(); String? masterPubkey; try { @@ -153,7 +153,7 @@ class CrossSigning { // we don't care about signing other cross-signing keys } else { // okay, we'll sign a device key with our self signing key - selfSigningKey ??= base64.decode(await encryption.ssss + selfSigningKey ??= base64decodeUnpadded(await encryption.ssss .getCached(EventTypes.CrossSigningSelfSigning) ?? ''); if (selfSigningKey.isNotEmpty) { @@ -163,7 +163,7 @@ class CrossSigning { } } else if (key is CrossSigningKey && key.usage.contains('master')) { // we are signing someone elses master key - userSigningKey ??= base64.decode(await encryption.ssss + userSigningKey ??= base64decodeUnpadded(await encryption.ssss .getCached(EventTypes.CrossSigningUserSigning) ?? ''); if (userSigningKey.isNotEmpty) { diff --git a/lib/encryption/key_manager.dart b/lib/encryption/key_manager.dart index 15977bb9..5f9f6547 100644 --- a/lib/encryption/key_manager.dart +++ b/lib/encryption/key_manager.dart @@ -18,6 +18,7 @@ import 'dart:convert'; +import 'package:matrix/encryption/utils/base64_unpadded.dart'; import 'package:matrix/encryption/utils/stored_inbound_group_session.dart'; import 'package:olm/olm.dart' as olm; import 'package:collection/collection.dart'; @@ -49,7 +50,7 @@ class KeyManager { BackupAlgorithm.mMegolmBackupV1Curve25519AesSha2) { return false; } - return keyObj.init_with_private_key(base64.decode(secret)) == + return keyObj.init_with_private_key(base64decodeUnpadded(secret)) == info.authData['public_key']; } catch (_) { return false; @@ -579,7 +580,7 @@ class KeyManager { return; } final privateKey = - base64.decode((await encryption.ssss.getCached(megolmKey))!); + base64decodeUnpadded((await encryption.ssss.getCached(megolmKey))!); final decryption = olm.PkDecryption(); final info = await getRoomKeysBackupInfo(); String backupPubKey; @@ -723,7 +724,7 @@ class KeyManager { return; // nothing to do } final privateKey = - base64.decode((await encryption.ssss.getCached(megolmKey))!); + base64decodeUnpadded((await encryption.ssss.getCached(megolmKey))!); // decryption is needed to calculate the public key and thus see if the claimed information is in fact valid final decryption = olm.PkDecryption(); final info = await getRoomKeysBackupInfo(false); diff --git a/lib/encryption/ssss.dart b/lib/encryption/ssss.dart index fc591522..60e4dabf 100644 --- a/lib/encryption/ssss.dart +++ b/lib/encryption/ssss.dart @@ -24,6 +24,7 @@ import 'dart:typed_data'; import 'package:base58check/base58.dart'; import 'package:crypto/crypto.dart'; import 'package:collection/collection.dart'; +import 'package:matrix/encryption/utils/base64_unpadded.dart'; import '../matrix.dart'; import '../src/utils/crypto/crypto.dart' as uc; @@ -85,7 +86,7 @@ class SSSS { [String? ivStr]) async { Uint8List iv; if (ivStr != null) { - iv = base64.decode(ivStr); + iv = base64decodeUnpadded(ivStr); } else { iv = Uint8List.fromList(uc.secureRandomBytes(16)); } @@ -108,15 +109,15 @@ class SSSS { static Future decryptAes( _Encrypted data, Uint8List key, String name) async { final keys = deriveKeys(key, name); - final cipher = base64.decode(data.ciphertext); + final cipher = base64decodeUnpadded(data.ciphertext); final hmac = base64 .encode(Hmac(sha256, keys.hmacKey).convert(cipher).bytes) .replaceAll(RegExp(r'=+$'), ''); if (hmac != data.mac.replaceAll(RegExp(r'=+$'), '')) { throw Exception('Bad MAC'); } - final decipher = - await uc.aesCtr.encrypt(cipher, keys.aesKey, base64.decode(data.iv)); + final decipher = await uc.aesCtr + .encrypt(cipher, keys.aesKey, base64decodeUnpadded(data.iv)); return String.fromCharCodes(decipher); } diff --git a/lib/encryption/utils/base64_unpadded.dart b/lib/encryption/utils/base64_unpadded.dart new file mode 100644 index 00000000..c66b64e8 --- /dev/null +++ b/lib/encryption/utils/base64_unpadded.dart @@ -0,0 +1,13 @@ +import 'dart:convert'; +import 'dart:typed_data'; + +/// decodes base64 +/// +/// Dart's native [base64.decode] requires a padded base64 input String. +/// This function allows unpadded base64 too. +/// +/// See: https://github.com/dart-lang/sdk/issues/39510 +Uint8List base64decodeUnpadded(String s) { + final needEquals = (4 - (s.length % 4)) % 4; + return base64.decode(s + ('=' * needEquals)); +} diff --git a/lib/encryption/utils/bootstrap.dart b/lib/encryption/utils/bootstrap.dart index aef6518c..515d0744 100644 --- a/lib/encryption/utils/bootstrap.dart +++ b/lib/encryption/utils/bootstrap.dart @@ -26,6 +26,7 @@ import '../encryption.dart'; import '../ssss.dart'; import '../key_manager.dart'; import '../../matrix.dart'; +import 'base64_unpadded.dart'; enum BootstrapState { /// Is loading. @@ -385,7 +386,7 @@ class Bootstrap { } } else { Logs().v('Get stored key...'); - masterSigningKey = base64.decode( + masterSigningKey = base64decodeUnpadded( await newSsssKey?.getStored(EventTypes.CrossSigningMasterKey) ?? ''); if (masterSigningKey.isEmpty) { diff --git a/lib/src/utils/crypto/encrypted_file.dart b/lib/src/utils/crypto/encrypted_file.dart index 754e5ced..d3dd874b 100644 --- a/lib/src/utils/crypto/encrypted_file.dart +++ b/lib/src/utils/crypto/encrypted_file.dart @@ -18,6 +18,8 @@ import 'dart:typed_data'; import 'dart:convert'; +import 'package:matrix/encryption/utils/base64_unpadded.dart'; + import 'crypto.dart'; class EncryptedFile { @@ -52,7 +54,7 @@ Future decryptFile(EncryptedFile input) async { return null; } - final key = base64.decode(base64.normalize(input.k)); - final iv = base64.decode(base64.normalize(input.iv)); + final key = base64decodeUnpadded(base64.normalize(input.k)); + final iv = base64decodeUnpadded(base64.normalize(input.iv)); return await aesCtr.encrypt(input.data, key, iv); } diff --git a/test/encryption/utils_test.dart b/test/encryption/utils_test.dart new file mode 100644 index 00000000..242703ac --- /dev/null +++ b/test/encryption/utils_test.dart @@ -0,0 +1,43 @@ +/* + * Famedly Matrix SDK + * Copyright (C) 2022 Famedly GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import 'dart:convert'; + +import 'package:matrix/encryption/utils/base64_unpadded.dart'; +import 'package:test/test.dart'; + +void main() { + group('Utils', () { + const base64input = 'foobar'; + final utf8codec = Utf8Codec(); + test('base64 padded', () { + final paddedBase64 = base64.encode(base64input.codeUnits); + + final decodedPadded = + utf8codec.decode(base64decodeUnpadded(paddedBase64)); + expect(decodedPadded, base64input, reason: 'Padded base64 decode'); + }); + + test('base64 unpadded', () { + const unpaddedBase64 = 'Zm9vYmFy'; + final decodedUnpadded = + utf8codec.decode(base64decodeUnpadded(unpaddedBase64)); + expect(decodedUnpadded, base64input, reason: 'Unpadded base64 decode'); + }); + }); +}