diff --git a/lib/model/tokens/steam_token.dart b/lib/model/tokens/steam_token.dart index d2855b84..c5fe57b2 100644 --- a/lib/model/tokens/steam_token.dart +++ b/lib/model/tokens/steam_token.dart @@ -40,13 +40,14 @@ class SteamToken extends TOTPToken { @override bool get isPrivacyIdeaToken => false; + @override + Null get serial => null; static String get tokenType => TokenTypes.STEAM.name; static const String steamAlphabet = "23456789BCDFGHJKMNPQRTVWXY"; SteamToken({ required super.id, required super.secret, - super.serial, super.containerSerial, super.checkedContainer, String? type, @@ -87,7 +88,6 @@ class SteamToken extends TOTPToken { Algorithms? algorithm, // unused steam tokens always have SHA1 algorithm }) { return SteamToken( - serial: serial ?? this.serial, label: label ?? this.label, issuer: issuer ?? this.issuer, containerSerial: containerSerial != null ? containerSerial() : this.containerSerial, @@ -180,7 +180,6 @@ class SteamToken extends TOTPToken { return SteamToken( label: uriMap[Token.LABEL], issuer: uriMap[Token.ISSUER], - serial: uriMap[Token.SERIAL], secret: uriMap[OTPToken.SECRET_BASE32], tokenImage: uriMap[Token.IMAGE], pin: uriMap[Token.PIN], diff --git a/lib/model/tokens/steam_token.g.dart b/lib/model/tokens/steam_token.g.dart index 8b1463e6..4310de14 100644 --- a/lib/model/tokens/steam_token.g.dart +++ b/lib/model/tokens/steam_token.g.dart @@ -9,7 +9,6 @@ part of 'steam_token.dart'; SteamToken _$SteamTokenFromJson(Map json) => SteamToken( id: json['id'] as String, secret: json['secret'] as String, - serial: json['serial'] as String?, containerSerial: json['containerSerial'] as String?, checkedContainer: (json['checkedContainer'] as List?) ?.map((e) => e as String) @@ -36,7 +35,6 @@ Map _$SteamTokenToJson(SteamToken instance) => 'issuer': instance.issuer, 'containerSerial': instance.containerSerial, 'id': instance.id, - 'serial': instance.serial, 'pin': instance.pin, 'isLocked': instance.isLocked, 'isHidden': instance.isHidden, diff --git a/lib/model/tokens/token.dart b/lib/model/tokens/token.dart index bcc76dbe..ec0829d4 100644 --- a/lib/model/tokens/token.dart +++ b/lib/model/tokens/token.dart @@ -162,9 +162,13 @@ abstract class Token with SortableMixin { other.label == label && other.issuer == issuer && other.pin == pin && other.isLocked == isLocked && other.tokenImage == tokenImage; /// This is used to identify the same token even if the id is different. - /// + /// The token should be considered the same if (the id is the same) or (the serial and issuer are the same). + /// if the id is different and the serial is not null, it should be depend on other factors like the secret and the algorithm. So the token should be recognized when a the same token without a serial is imported multiple times. bool? isSameTokenAs(Token other) { - if (serial != null && serial == other.serial && issuer == other.issuer) return true; + if (id == other.id) return true; + if (serial != null || other.serial != null) { + return (serial == other.serial && issuer == other.issuer); + } return null; } diff --git a/lib/utils/riverpod/riverpod_providers/generated_providers/token_container_notifier.g.dart b/lib/utils/riverpod/riverpod_providers/generated_providers/token_container_notifier.g.dart index 885fddfe..fff69949 100644 --- a/lib/utils/riverpod/riverpod_providers/generated_providers/token_container_notifier.g.dart +++ b/lib/utils/riverpod/riverpod_providers/generated_providers/token_container_notifier.g.dart @@ -7,7 +7,7 @@ part of 'token_container_notifier.dart'; // ************************************************************************** String _$tokenContainerNotifierHash() => - r'c6c5bf723ca21b6dfdad59a42ef3678b4f960996'; + r'11d1c6cf9c60aca062043bf2c8ece76da71b9709'; /// Copied from Dart SDK class _SystemHash { diff --git a/lib/utils/riverpod/riverpod_providers/generated_providers/token_notifier.g.dart b/lib/utils/riverpod/riverpod_providers/generated_providers/token_notifier.g.dart index c29a09a1..7a6e713f 100644 --- a/lib/utils/riverpod/riverpod_providers/generated_providers/token_notifier.g.dart +++ b/lib/utils/riverpod/riverpod_providers/generated_providers/token_notifier.g.dart @@ -6,7 +6,7 @@ part of 'token_notifier.dart'; // RiverpodGenerator // ************************************************************************** -String _$tokenNotifierHash() => r'd5ee1c8cb58e467fe230dd97341996d08e2b9dba'; +String _$tokenNotifierHash() => r'38949e986886587dac4ced5de73f58946fc1e259'; /// Copied from Dart SDK class _SystemHash { diff --git a/test/unit_test/model/token/day_password_test.dart b/test/unit_test/model/token/day_password_test.dart index 177d853e..5c095baf 100644 --- a/test/unit_test/model/token/day_password_test.dart +++ b/test/unit_test/model/token/day_password_test.dart @@ -11,13 +11,7 @@ import 'package:privacyidea_authenticator/model/tokens/otp_token.dart'; import 'package:privacyidea_authenticator/model/tokens/token.dart'; import 'package:privacyidea_authenticator/model/tokens/totp_token.dart'; -void main() { - _testDayPasswordToken(); -} - -void _testDayPasswordToken() { - group('Day password creation/method', () { - final dayPasswordToken = DayPasswordToken( +DayPasswordToken get dayPasswordToken => DayPasswordToken( period: const Duration(hours: 24), viewMode: DayPasswordTokenViewMode.VALIDUNTIL, label: 'label', @@ -32,6 +26,12 @@ void _testDayPasswordToken() { isLocked: false, // if pin is true, its automatically forced to be locked=true folderId: 0, ); +void main() { + _testDayPasswordToken(); +} + +void _testDayPasswordToken() { + group('Day password creation', () { test('constructor', () { expect(dayPasswordToken.period, const Duration(hours: 24)); expect(dayPasswordToken.viewMode, DayPasswordTokenViewMode.VALIDUNTIL); @@ -76,6 +76,8 @@ void _testDayPasswordToken() { expect(totpCopy.isLocked, true); expect(totpCopy.folderId, 1); }); + }); + group('serialization', () { group('fromUriMap', () { test('with full map', () { final uriMap = { @@ -175,6 +177,18 @@ void _testDayPasswordToken() { expect(() => DayPasswordToken.fromOtpAuthMap(uriMap), throwsA(isA())); }); }); + test('toUriMap', () { + final uriMap = dayPasswordToken.toOtpAuthMap(); + expect(uriMap[Token.LABEL], 'label'); + expect(uriMap[Token.ISSUER], 'issuer'); + expect(uriMap[Token.OTPAUTH_TYPE], 'DAYPASSWORD'); + expect(uriMap[Token.PIN], Token.PIN_VALUE_TRUE); + expect(uriMap[Token.IMAGE], 'example.png'); + expect(uriMap[OTPToken.ALGORITHM], 'SHA1'); + expect(uriMap[OTPToken.DIGITS], '6'); + expect(uriMap[OTPToken.SECRET_BASE32], Encodings.base32.encode(utf8.encode('secret'))); + expect(uriMap[TOTPToken.PERIOD_SECONDS], '86400'); + }); test('fromJson', () { final totpJson = { 'period': 11000000, @@ -223,6 +237,151 @@ void _testDayPasswordToken() { expect(totpJson['folderId'], 0); }); }); + group('isSameTokenAs', () { + test('no serial | same id | same parameters', () { + // No serial. Should recognize by id or parameters + final dayPasswordToken = DayPasswordToken( + period: const Duration(hours: 24), + viewMode: DayPasswordTokenViewMode.VALIDUNTIL, + label: 'label', + issuer: 'issuer', + id: 'id', + algorithm: Algorithms.SHA1, + digits: 6, + secret: 'secret', + pin: true, + tokenImage: 'example.png', + sortIndex: 0, + isLocked: true, + folderId: 0, + ); + + expect(dayPasswordToken.isSameTokenAs(dayPasswordToken.copyWith()), true); + }); + test('no serial | same id | different parameters', () { + // No serial. Should recognize by id + final dayPasswordToken = DayPasswordToken( + period: const Duration(hours: 24), + viewMode: DayPasswordTokenViewMode.VALIDUNTIL, + label: 'label', + issuer: 'issuer', + id: 'id', + algorithm: Algorithms.SHA1, + digits: 6, + secret: 'secret', + pin: true, + tokenImage: 'example.png', + sortIndex: 0, + isLocked: true, + folderId: 0, + ); + + expect(dayPasswordToken.isSameTokenAs(dayPasswordToken.copyWith(algorithm: Algorithms.SHA256)), true); + }); + test('no serial | different id | same parameters', () { + // No serial, different id. Should recognize by parameters + final dayPasswordToken = DayPasswordToken( + period: const Duration(hours: 24), + viewMode: DayPasswordTokenViewMode.VALIDUNTIL, + label: 'label', + issuer: 'issuer', + id: 'id', + algorithm: Algorithms.SHA1, + digits: 6, + secret: 'secret', + pin: true, + tokenImage: 'example.png', + sortIndex: 0, + isLocked: true, + folderId: 0, + ); + + expect(dayPasswordToken.isSameTokenAs(dayPasswordToken.copyWith(id: 'id2')), true); + }); + test('no serial | different id | different parameters', () { + // No serial, different id, different parameters. Should not recognize + final dayPasswordToken = DayPasswordToken( + period: const Duration(hours: 24), + viewMode: DayPasswordTokenViewMode.VALIDUNTIL, + label: 'label', + issuer: 'issuer', + id: 'id', + algorithm: Algorithms.SHA1, + digits: 6, + secret: 'secret', + pin: true, + tokenImage: 'example.png', + sortIndex: 0, + isLocked: true, + folderId: 0, + ); + + expect(dayPasswordToken.isSameTokenAs(dayPasswordToken.copyWith(id: 'id2', algorithm: Algorithms.SHA256)), false); + }); + test('same serial | different id | different parameters', () { + // Different id, different parameters. Should recognize by serial + final dayPasswordToken = DayPasswordToken( + period: const Duration(hours: 24), + viewMode: DayPasswordTokenViewMode.VALIDUNTIL, + label: 'label', + issuer: 'issuer', + serial: 'serial', + id: 'id', + algorithm: Algorithms.SHA1, + digits: 6, + secret: 'secret', + pin: true, + tokenImage: 'example.png', + sortIndex: 0, + isLocked: true, + folderId: 0, + ); + + expect(dayPasswordToken.isSameTokenAs(dayPasswordToken.copyWith(id: 'id2', algorithm: Algorithms.SHA256)), true); + }); + test('different serial | same id | different parameters', () { + // Different serial, different parameters. Should recognize by id + final dayPasswordToken = DayPasswordToken( + period: const Duration(hours: 24), + viewMode: DayPasswordTokenViewMode.VALIDUNTIL, + label: 'label', + issuer: 'issuer', + serial: 'serial', + id: 'id', + algorithm: Algorithms.SHA1, + digits: 6, + secret: 'secret', + pin: true, + tokenImage: 'example.png', + sortIndex: 0, + isLocked: true, + folderId: 0, + ); + + expect(dayPasswordToken.isSameTokenAs(dayPasswordToken.copyWith(serial: 'serial2', algorithm: Algorithms.SHA256)), true); + }); + test('different serial | different id | same parameters', () { + // Different serial, different id. Should NOT recognize by parameters + final dayPasswordToken = DayPasswordToken( + period: const Duration(hours: 24), + viewMode: DayPasswordTokenViewMode.VALIDUNTIL, + label: 'label', + issuer: 'issuer', + serial: 'serial', + id: 'id', + algorithm: Algorithms.SHA1, + digits: 6, + secret: 'secret', + pin: true, + tokenImage: 'example.png', + sortIndex: 0, + isLocked: true, + folderId: 0, + ); + + expect(dayPasswordToken.isSameTokenAs(dayPasswordToken.copyWith(serial: 'serial2', id: 'id2')), false); + }); + }); group('Calculate day password values', () { // Basicly the day password is a HOTP token but the counter is calculated based on the current time. // So we can test day password token by comparing its OTP value with a HOTP value with the same counter. diff --git a/test/unit_test/model/token/hotp_token_test.dart b/test/unit_test/model/token/hotp_token_test.dart index 2883d3db..394b9f43 100644 --- a/test/unit_test/model/token/hotp_token_test.dart +++ b/test/unit_test/model/token/hotp_token_test.dart @@ -8,13 +8,7 @@ import 'package:privacyidea_authenticator/model/tokens/hotp_token.dart'; import 'package:privacyidea_authenticator/model/tokens/otp_token.dart'; import 'package:privacyidea_authenticator/model/tokens/token.dart'; -void main() { - _testHotpToken(); -} - -void _testHotpToken() { - group('HOTP Token creation/method', () { - final hotpToken = HOTPToken( +HOTPToken get hotpToken => HOTPToken( counter: 1, label: 'label', issuer: 'issuer', @@ -29,6 +23,13 @@ void _testHotpToken() { isLocked: false, folderId: 0, ); + +void main() { + _testHotpToken(); +} + +void _testHotpToken() { + group('HOTP Token creation', () { test('constructor', () { expect(hotpToken.counter, 1); expect(hotpToken.label, 'label'); @@ -77,6 +78,8 @@ void _testHotpToken() { expect(hotpCopy.isLocked, true); expect(hotpCopy.folderId, 1); }); + }); + group('serialization', () { group('fromUriMap', () { test('with full map', () { final uriMap = { @@ -164,6 +167,155 @@ void _testHotpToken() { expect(() => HOTPToken.fromOtpAuthMap(uriMap), throwsArgumentError); }); }); + test('toUriMap', () { + final uriMap = hotpToken.toOtpAuthMap(); + expect(uriMap[Token.LABEL], 'label'); + expect(uriMap[Token.ISSUER], 'issuer'); + expect(uriMap[Token.OTPAUTH_TYPE], 'HOTP'); + expect(uriMap[Token.PIN], Token.PIN_VALUE_TRUE); + expect(uriMap[Token.IMAGE], 'example.png'); + expect(uriMap[OTPToken.ALGORITHM], 'SHA1'); + expect(uriMap[OTPToken.SECRET_BASE32], Encodings.base32.encode(utf8.encode('secret'))); + expect(uriMap[OTPToken.DIGITS], '6'); + expect(uriMap[HOTPToken.COUNTER], '1'); + }); + test('fromJson', () { + final hotpJson = { + 'label': 'label', + 'issuer': 'issuer', + 'id': 'id', + 'type': 'HOTP', + 'algorithm': 'SHA256', + 'digits': 8, + 'secret': 'ONSWG4TFOQ======', + 'counter': 5, + 'pin': false, + }; + final hotpFromJson = HOTPToken.fromJson(hotpJson); + expect(hotpFromJson.counter, 5); + expect(hotpFromJson.label, 'label'); + expect(hotpFromJson.issuer, 'issuer'); + expect(hotpFromJson.algorithm, Algorithms.SHA256); + expect(hotpFromJson.digits, 8); + expect(hotpFromJson.secret, 'ONSWG4TFOQ======'); + expect(hotpFromJson.type, 'HOTP'); + expect(hotpFromJson.pin, false); + }); + test('toJson', () { + final hotpJson = hotpToken.toJson(); + expect(hotpJson['label'], 'label'); + expect(hotpJson['issuer'], 'issuer'); + expect(hotpJson['id'], 'id'); + expect(hotpJson['type'], 'HOTP'); + expect(hotpJson['algorithm'], 'SHA1'); + expect(hotpJson['digits'], 6); + expect(hotpJson['secret'], 'secret'); + expect(hotpJson['counter'], 1); + expect(hotpJson['pin'], true); + }); + }); + group('isSameTokenAs', () { + test('no serial | same id | same parameters', () { + // No serial. Should recognize by id or parameters + final hotpToken = HOTPToken( + id: 'id', + label: 'label', + issuer: 'issuer', + algorithm: Algorithms.SHA1, + digits: 6, + secret: 'secret', + counter: 0, + ); + + expect(hotpToken.isSameTokenAs(hotpToken), true); + }); + test('no serial | same id | different parameters', () { + // No serial. Should recognize by id + final hotpToken = HOTPToken( + id: 'id', + label: 'label', + issuer: 'issuer', + algorithm: Algorithms.SHA1, + digits: 6, + secret: 'secret', + counter: 0, + ); + + expect(hotpToken.isSameTokenAs(hotpToken.copyWith(algorithm: Algorithms.SHA256)), true); + }); + test('no serial | different id | same parameters', () { + // No serial, different id. Should recognize by parameters + final hotpToken = HOTPToken( + id: 'id', + label: 'label', + issuer: 'issuer', + algorithm: Algorithms.SHA1, + digits: 6, + secret: 'secret', + counter: 0, + ); + + expect(hotpToken.isSameTokenAs(hotpToken.copyWith(id: 'id2')), true); + }); + test('no serial | different id | different parameters', () { + // No serial, different id, different parameters. Should not recognize + final hotpToken = HOTPToken( + id: 'id', + label: 'label', + issuer: 'issuer', + algorithm: Algorithms.SHA1, + digits: 6, + secret: 'secret', + counter: 0, + ); + + expect(hotpToken.isSameTokenAs(hotpToken.copyWith(id: 'id2', algorithm: Algorithms.SHA256)), false); + }); + test('same serial | different id | different parameters', () { + // Different id, different parameters. Should recognize by serial + final hotpToken = HOTPToken( + id: 'id', + label: 'label', + issuer: 'issuer', + serial: 'serial', + algorithm: Algorithms.SHA1, + digits: 6, + secret: 'secret', + counter: 0, + ); + + expect(hotpToken.isSameTokenAs(hotpToken.copyWith(id: 'id2', algorithm: Algorithms.SHA256)), true); + }); + test('different serial | same id | different parameters', () { + // Different serial, different parameters. Should recognize by id + final hotpToken = HOTPToken( + id: 'id', + label: 'label', + issuer: 'issuer', + serial: 'serial', + algorithm: Algorithms.SHA1, + digits: 6, + secret: 'secret', + counter: 0, + ); + + expect(hotpToken.isSameTokenAs(hotpToken.copyWith(serial: 'serial2', algorithm: Algorithms.SHA256)), true); + }); + test('different serial | different id | same parameters', () { + // Different serial, different id. Should NOT recognize by parameters + final hotpToken = HOTPToken( + id: 'id', + label: 'label', + issuer: 'issuer', + serial: 'serial', + algorithm: Algorithms.SHA1, + digits: 6, + secret: 'secret', + counter: 0, + ); + + expect(hotpToken.isSameTokenAs(hotpToken.copyWith(serial: 'serial2', id: 'id2')), false); + }); }); group('Calculate hotp values', () { group('different couters 6 digits', () { diff --git a/test/unit_test/model/token/push_token_test.dart b/test/unit_test/model/token/push_token_test.dart index a330822b..1ede049d 100644 --- a/test/unit_test/model/token/push_token_test.dart +++ b/test/unit_test/model/token/push_token_test.dart @@ -3,13 +3,7 @@ import 'package:privacyidea_authenticator/model/enums/push_token_rollout_state.d import 'package:privacyidea_authenticator/model/tokens/push_token.dart'; import 'package:privacyidea_authenticator/model/tokens/token.dart'; -void main() { - _testPushToken(); -} - -void _testPushToken() { - group('Push Token creation/method', () { - final pushToken = PushToken( +PushToken get pushToken => PushToken( serial: 'serial', expirationDate: DateTime(2017, 9, 7, 17, 30), label: 'label', @@ -29,6 +23,12 @@ void _testPushToken() { isLocked: true, pin: true, ); +void main() { + _testPushToken(); +} + +void _testPushToken() { + group('Push Token creation', () { test('constructor', () { expect(pushToken.serial, 'serial'); expect(pushToken.expirationDate, DateTime(2017, 9, 7, 17, 30)); @@ -90,6 +90,8 @@ void _testPushToken() { expect(copy.isLocked, false); expect(copy.pin, false); }); + }); + group('serialization', () { test('fromJson', () { final json = { "label": "label", @@ -193,5 +195,146 @@ void _testPushToken() { expect(() => PushToken.fromOtpAuthMap(uriMap), throwsA(isA())); }); }); + + test('toUriMap', () { + final token = PushToken( + serial: 'serial', + expirationDate: DateTime(2017, 9, 7, 17, 30), + label: 'label', + issuer: 'issuer', + id: 'id', + sslVerify: true, + enrollmentCredentials: 'enrollmentCredentials', + url: Uri.parse('http://www.example.com'), + publicServerKey: 'publicServerKey', + publicTokenKey: 'publicTokenKey', + privateTokenKey: 'privateTokenKey', + isRolledOut: true, + rolloutState: PushTokenRollOutState.rolloutNotStarted, + sortIndex: 0, + tokenImage: 'example.png', + folderId: 0, + isLocked: true, + pin: true, + ); + final uriMap = token.toOtpAuthMap(); + expect(uriMap[Token.OTPAUTH_TYPE], 'PIPUSH'); + expect(uriMap[Token.LABEL], 'label'); + expect(uriMap[Token.ISSUER], 'issuer'); + expect(uriMap[Token.SERIAL], 'serial'); + expect(uriMap[PushToken.SSL_VERIFY], 'True'); + expect(uriMap[PushToken.ENROLLMENT_CREDENTIAL], 'enrollmentCredentials'); + expect(uriMap[PushToken.ROLLOUT_URL], 'http://www.example.com'); + expect(uriMap[PushToken.TTL_MINUTES], '30'); + expect(uriMap[PushToken.VERSION], '1'); + }); + test('fromJson', () { + final json = { + 'label': 'label', + 'issuer': 'issuer', + 'id': 'id', + 'isLocked': true, + 'pin': true, + 'tokenImage': 'example.png', + 'folderId': 0, + 'sortIndex': 0, + 'type': 'PIPUSH', + 'expirationDate': '2017-09-07T17:30:00.000', + 'serial': 'serial', + 'sslVerify': true, + 'enrollmentCredentials': 'enrollmentCredentials', + 'url': 'http://www.example.com', + 'isRolledOut': true, + 'rolloutState': 'generatingRSAKeyPair', + 'publicServerKey': 'publicServerKey', + 'privateTokenKey': 'privateTokenKey', + 'publicTokenKey': 'publicTokenKey', + }; + final token = PushToken.fromJson(json); + expect(token.label, 'label'); + expect(token.issuer, 'issuer'); + expect(token.id, 'id'); + expect(token.isLocked, true); + expect(token.pin, true); + expect(token.tokenImage, 'example.png'); + expect(token.folderId, 0); + expect(token.sortIndex, 0); + expect(token.type, 'PIPUSH'); + expect(token.expirationDate.toString(), DateTime(2017, 9, 7, 17, 30).toString()); + expect(token.serial, 'serial'); + expect(token.sslVerify, true); + expect(token.enrollmentCredentials, 'enrollmentCredentials'); + expect(token.url, Uri.parse('http://www.example.com')); + expect(token.isRolledOut, true); + expect(token.rolloutState, + PushTokenRollOutState.generatingRSAKeyPairFailed); // When loading from json, an processing state should be converted to a failed state. + expect(token.publicServerKey, 'publicServerKey'); + expect(token.privateTokenKey, 'privateTokenKey'); + expect(token.publicTokenKey, 'publicTokenKey'); + }); + test('toJson', () { + final tokenJson = pushToken.toJson(); + final json = { + "checkedContainer": [], + "containerSerial": null, + "label": "label", + "issuer": "issuer", + "id": "id", + "pin": true, + "isLocked": true, + "isHidden": false, + "tokenImage": "example.png", + "folderId": 0, + "sortIndex": 0, + "origin": null, + "type": "PIPUSH", + "expirationDate": "2017-09-07T17:30:00.000", + "serial": "serial", + "fbToken": null, + "sslVerify": true, + "enrollmentCredentials": "enrollmentCredentials", + "url": "http://www.example.com", + "isRolledOut": true, + "rolloutState": "rolloutNotStarted", + "publicServerKey": "publicServerKey", + "privateTokenKey": "privateTokenKey", + "publicTokenKey": "publicTokenKey" + }; + for (final key in json.keys) { + expect(tokenJson[key], json[key]); + } + }); + }); + group('isSameTokenAs', () { + test('same serial | different id | different parameters', () { + // Different id, different parameters. Should recognize by serial + final pushToken = PushToken( + serial: 'serial', + id: 'id', + privateTokenKey: 'privateTokenKey', + ); + + expect(pushToken.isSameTokenAs(pushToken.copyWith(id: 'id2', privateTokenKey: 'privateTokenKey2')), true); + }); + test('different serial | same id | different parameters', () { + // Different serial, different parameters. Should recognize by id + final pushToken = PushToken( + serial: 'serial', + id: 'id', + privateTokenKey: 'privateTokenKey', + ); + + expect(pushToken.isSameTokenAs(pushToken.copyWith(serial: 'serial2', privateTokenKey: 'privateTokenKey2')), true); + }); + test('different serial | different id | same parameters', () { + // Different serial, different id. Should NOT recognize by parameters + final pushToken = PushToken( + serial: 'serial', + id: 'id', + privateTokenKey: 'privateTokenKey', + ); + + expect(pushToken.isSameTokenAs(pushToken.copyWith(serial: 'serial2', id: 'id2')), false); + }); }); } diff --git a/test/unit_test/model/token/steam_token_test.dart b/test/unit_test/model/token/steam_token_test.dart index 09e4838d..a178012a 100644 --- a/test/unit_test/model/token/steam_token_test.dart +++ b/test/unit_test/model/token/steam_token_test.dart @@ -9,24 +9,24 @@ import 'package:privacyidea_authenticator/model/tokens/steam_token.dart'; import 'package:privacyidea_authenticator/model/tokens/token.dart'; import 'package:privacyidea_authenticator/model/tokens/totp_token.dart'; +SteamToken get steamToken => SteamToken( + label: 'label', + issuer: 'issuer', + id: 'id', + secret: 'secret', + pin: false, + tokenImage: 'example.png', + sortIndex: 0, + isLocked: false, + folderId: 0, + ); void main() { _testSteamToken(); } void _testSteamToken() { group('Steam Token', () { - group('TOTP Token creation/method', () { - final steamToken = SteamToken( - label: 'label', - issuer: 'issuer', - id: 'id', - secret: 'secret', - pin: false, - tokenImage: 'example.png', - sortIndex: 0, - isLocked: false, - folderId: 0, - ); + group('TOTP Token creation', () { test('constructor', () { expect(steamToken.period, 30); // default period expect(steamToken.label, 'label'); @@ -71,6 +71,8 @@ void _testSteamToken() { expect(totpCopy.isLocked, true); expect(totpCopy.folderId, 1); }); + }); + group('Serialization', () { group('fromUriMap', () { test('with full map', () { final uriMap = { @@ -107,6 +109,15 @@ void _testSteamToken() { expect(() => TOTPToken.fromOtpAuthMap(uriMap), throwsA(isA())); }); }); + test('toUriMap', () { + final totpUriMap = steamToken.toOtpAuthMap(); + expect(totpUriMap[Token.LABEL], 'label'); + expect(totpUriMap[Token.ISSUER], 'issuer'); + expect(totpUriMap[Token.OTPAUTH_TYPE], 'STEAM'); + expect(totpUriMap[Token.PIN], false); + expect(totpUriMap[Token.IMAGE], 'example.png'); + expect(totpUriMap[OTPToken.SECRET_BASE32], 'ONSWG4TFOQ======'); + }); test('fromJson', () { final steamJson = { 'label': 'label', @@ -149,6 +160,52 @@ void _testSteamToken() { expect(totpJson['folderId'], 0); }); }); + group('isSameTokenAs', () { + test('no serial | same id | same parameters', () { + // No serial. Should recognize by id or parameters + final steamToken = SteamToken( + label: 'label', + issuer: 'issuer', + id: 'id', + secret: 'secret', + ); + + expect(steamToken.isSameTokenAs(steamToken.copyWith()), isTrue); + }); + test('no serial | same id | different parameters', () { + // No serial. Should recognize by id + final steamToken = SteamToken( + label: 'label', + issuer: 'issuer', + id: 'id', + secret: 'secret', + ); + + expect(steamToken.isSameTokenAs(steamToken.copyWith(secret: 'secret2')), isTrue); + }); + test('no serial | different id | same parameters', () { + // No serial, different id. Should recognize by parameters + final steamToken = SteamToken( + label: 'label', + issuer: 'issuer', + id: 'id', + secret: 'secret', + ); + + expect(steamToken.isSameTokenAs(steamToken.copyWith(id: 'id2')), isTrue); + }); + test('no serial | different id | different parameters', () { + // No serial, different id, different parameters. Should not recognize + final steamToken = SteamToken( + label: 'label', + issuer: 'issuer', + id: 'id', + secret: 'secret', + ); + + expect(steamToken.isSameTokenAs(steamToken.copyWith(id: 'id2', secret: 'secret2')), isFalse); + }); + }); test('otpValue', () { final time = DateTime.fromMillisecondsSinceEpoch(1712666212056); diff --git a/test/unit_test/model/token/totp_token_test.dart b/test/unit_test/model/token/totp_token_test.dart index bfbcfb43..74cf010a 100644 --- a/test/unit_test/model/token/totp_token_test.dart +++ b/test/unit_test/model/token/totp_token_test.dart @@ -10,13 +10,7 @@ import 'package:privacyidea_authenticator/model/tokens/otp_token.dart'; import 'package:privacyidea_authenticator/model/tokens/token.dart'; import 'package:privacyidea_authenticator/model/tokens/totp_token.dart'; -void main() { - _testTotpToken(); -} - -void _testTotpToken() { - group('TOTP Token creation/method', () { - final totpToken = TOTPToken( +TOTPToken get totpToken => TOTPToken( period: 30, label: 'label', issuer: 'issuer', @@ -30,6 +24,13 @@ void _testTotpToken() { isLocked: false, folderId: 0, ); + +void main() { + _testTotpToken(); +} + +void _testTotpToken() { + group('TOTP Token creation', () { test('constructor', () { expect(totpToken.period, 30); expect(totpToken.label, 'label'); @@ -74,6 +75,8 @@ void _testTotpToken() { expect(totpCopy.isLocked, true); expect(totpCopy.folderId, 1); }); + }); + group('serialization', () { group('fromUriMap', () { test('with full map', () { final uriMap = { @@ -159,6 +162,18 @@ void _testTotpToken() { expect(() => TOTPToken.fromOtpAuthMap(uriMap), throwsA(isA())); }); }); + test('toUriMap', () { + final totpUriMap = totpToken.toOtpAuthMap(); + expect(totpUriMap[Token.LABEL], 'label'); + expect(totpUriMap[Token.ISSUER], 'issuer'); + expect(totpUriMap[Token.OTPAUTH_TYPE], 'TOTP'); + expect(totpUriMap[Token.PIN], false); + expect(totpUriMap[Token.IMAGE], 'example.png'); + expect(totpUriMap[OTPToken.ALGORITHM], 'SHA1'); + expect(totpUriMap[OTPToken.DIGITS], 6); + expect(totpUriMap[OTPToken.SECRET_BASE32], 'ONSWG4TFOQ======'); + expect(totpUriMap[TOTPToken.PERIOD_SECONDS], 30); + }); test('fromJson', () { final totpJson = { 'period': 11, @@ -207,7 +222,109 @@ void _testTotpToken() { expect(totpJson['folderId'], 0); }); }); + group('isSameTokenAs', () { + test('no serial | same id | same parameters', () { + // No serial. Should recognize by id or parameters + final totpToken = TOTPToken( + period: 30, + label: 'label', + issuer: 'issuer', + id: 'id', + algorithm: Algorithms.SHA1, + digits: 6, + secret: 'secret', + ); + + expect(totpToken.isSameTokenAs(totpToken.copyWith()), isTrue); + }); + test('no serial | same id | different parameters', () { + // No serial. Should recognize by id + final totpToken = TOTPToken( + period: 30, + label: 'label', + issuer: 'issuer', + id: 'id', + algorithm: Algorithms.SHA1, + digits: 6, + secret: 'secret', + ); + expect(totpToken.isSameTokenAs(totpToken.copyWith(algorithm: Algorithms.SHA256)), isTrue); + }); + test('no serial | different id | same parameters', () { + // No serial, different id. Should recognize by parameters + final totpToken = TOTPToken( + period: 30, + label: 'label', + issuer: 'issuer', + id: 'id', + algorithm: Algorithms.SHA1, + digits: 6, + secret: 'secret', + ); + + expect(totpToken.isSameTokenAs(totpToken.copyWith(id: 'id2')), isTrue); + }); + test('no serial | different id | different parameters', () { + // No serial, different id, different parameters. Should not recognize + final totpToken = TOTPToken( + period: 30, + label: 'label', + issuer: 'issuer', + id: 'id', + algorithm: Algorithms.SHA1, + digits: 6, + secret: 'secret', + ); + + expect(totpToken.isSameTokenAs(totpToken.copyWith(id: 'id2', algorithm: Algorithms.SHA256)), isFalse); + }); + test('same serial | different id | different parameters', () { + // Different id, different parameters. Should recognize by serial + final totpToken = TOTPToken( + period: 30, + label: 'label', + issuer: 'issuer', + serial: 'serial', + id: 'id', + algorithm: Algorithms.SHA1, + digits: 6, + secret: 'secret', + ); + + expect(totpToken.isSameTokenAs(totpToken.copyWith(id: 'id2', algorithm: Algorithms.SHA256)), isTrue); + }); + test('different serial | same id | different parameters', () { + // Different serial, different parameters. Should recognize by id + final totpToken = TOTPToken( + period: 30, + label: 'label', + issuer: 'issuer', + serial: 'serial', + id: 'id', + algorithm: Algorithms.SHA1, + digits: 6, + secret: 'secret', + ); + + expect(totpToken.isSameTokenAs(totpToken.copyWith(serial: 'serial2', algorithm: Algorithms.SHA256)), isTrue); + }); + test('different serial | different id | same parameters', () { + // Different serial, different id. Should NOT recognize by parameters + final totpToken = TOTPToken( + period: 30, + label: 'label', + issuer: 'issuer', + serial: 'serial', + id: 'id', + algorithm: Algorithms.SHA1, + digits: 6, + secret: 'secret', + ); + + expect(totpToken.isSameTokenAs(totpToken.copyWith(serial: 'serial2', id: 'id2')), isFalse); + }); + }); group('Calculate TOTP Token values', () { // Basicly the TOTP token is a HOTP token but the counter is calculated based on the current time. // So we can test TOTP token by comparing its OTP value with a HOTP value with the same counter. diff --git a/test/unit_test/state_notifiers/token_container_notifier_test.dart b/test/unit_test/state_notifiers/token_container_notifier_test.dart index dd840e54..d3c69ff5 100644 --- a/test/unit_test/state_notifiers/token_container_notifier_test.dart +++ b/test/unit_test/state_notifiers/token_container_notifier_test.dart @@ -664,135 +664,137 @@ void _testTokenContainerNotifier() { expect(stateContainer.publicServerKey, expectedContainer.publicServerKey); expect(stateContainer.publicClientKey, isNotEmpty); }); - test('sync', () async { - // prepare - TestWidgetsFlutterBinding.ensureInitialized(); - var containerRepoState = _buildFinalizedContainerState(); - final containerToSync = containerRepoState.containerList.first as TokenContainerFinalized; - final mockContainerApi = MockTokenContainerApi(); - final updatedTokens = [ - HOTPToken( - id: 'ID01', - serial: "HOTPTOKEN01", - containerSerial: "CONTAINER01", - algorithm: Algorithms.SHA256, - digits: 6, - secret: "SECRET01", - counter: 8, - ), - TOTPToken( - id: "ID03", - serial: "TOTPTOKEN01", - period: 30, - algorithm: Algorithms.SHA256, - digits: 8, - secret: "SECRET03", - ), - ]; - when(mockContainerApi.sync(any, any)).thenAnswer( - (v) async => ContainerSyncUpdates( - containerSerial: 'CONTAINER01', - updatedTokens: updatedTokens, - deleteTokenSerials: ["HOTPTOKEN02"], - newPolicies: ContainerPolicies( + group('sync', () async { + test('sync', () async { + // prepare + TestWidgetsFlutterBinding.ensureInitialized(); + var containerRepoState = _buildFinalizedContainerState(); + final containerToSync = containerRepoState.containerList.first as TokenContainerFinalized; + final mockContainerApi = MockTokenContainerApi(); + final updatedTokens = [ + HOTPToken( + id: 'ID01', + serial: "HOTPTOKEN01", + containerSerial: "CONTAINER01", + algorithm: Algorithms.SHA256, + digits: 6, + secret: "SECRET01", + counter: 8, + ), + TOTPToken( + id: "ID03", + serial: "TOTPTOKEN01", + period: 30, + algorithm: Algorithms.SHA256, + digits: 8, + secret: "SECRET03", + ), + ]; + when(mockContainerApi.sync(any, any)).thenAnswer( + (v) async => ContainerSyncUpdates( + containerSerial: 'CONTAINER01', + updatedTokens: updatedTokens, + deleteTokenSerials: ["HOTPTOKEN02"], + newPolicies: ContainerPolicies( + rolloverAllowed: true, + initialTokenTransfer: true, + tokensDeletable: true, + unregisterAllowed: true, + ), + ), + ); + + final mockContainerRepo = _setupMockContainerRepo(() => containerRepoState, (state) => containerRepoState = state); + + final mockTokenContainerProvider = TokenContainerNotifier( + repoOverride: mockContainerRepo, + containerApiOverride: mockContainerApi, + eccUtilsOverride: EccUtils(), + ); + // prepare - token notifier + var repoTokens = { + 'ID01': HOTPToken( + id: 'ID01', + serial: "HOTPTOKEN01", + containerSerial: "CONTAINER01", + algorithm: Algorithms.SHA256, + digits: 8, + secret: "SECRET01", + counter: 10, + ), + "ID02": HOTPToken( + id: "ID02", + serial: "HOTPTOKEN02", + containerSerial: null, + algorithm: Algorithms.SHA256, + digits: 6, + secret: "SECRET02", + counter: 12, + ), + "ID04": TOTPToken( + id: "ID04", + serial: "TOTPTOKEN02", + period: 30, + algorithm: Algorithms.SHA512, + digits: 6, + secret: "SECRET04", + ), + }; + final mockTokenRepo = MockTokenRepository(); + when(mockTokenRepo.loadTokens()).thenAnswer((_) => Future.value(repoTokens.values.toList())); + when(mockTokenRepo.saveOrReplaceTokens(any)).thenAnswer((invocation) { + final tokens = invocation.positionalArguments[0] as List; + for (final token in tokens) { + repoTokens[token.id] = token; + } + return Future.value([]); + }); + + final mockTokenNotifier = TokenNotifier( + repoOverride: mockTokenRepo, + ); + + // prepare - settings notifier + final MockSettingsRepository mockSettingsRepo = MockSettingsRepository(); + when(mockSettingsRepo.loadSettings()).thenAnswer((_) => Future.value(SettingsState())); + when(mockSettingsRepo.saveSettings(any)).thenAnswer((invocation) => Future.value(invocation.positionalArguments[0])); + final SettingsNotifier settingsNotifier = SettingsNotifier(repoOverride: mockSettingsRepo); + + // prepare - provider container + final providerContainer = ProviderContainer( + overrides: [ + tokenContainerProvider.overrideWith(() => mockTokenContainerProvider), + tokenProvider.overrideWith(() => mockTokenNotifier), + settingsProvider.overrideWith(() => settingsNotifier), + ], + ); + + // act + var tokenState = providerContainer.read(tokenProvider); + await providerContainer.read(tokenContainerProvider.notifier).sync(tokenState: tokenState, isManually: false); + + // assert + final expectedStateUnordered = TokenState(tokens: [...updatedTokens, repoTokens["ID04"]!]); + final containerState = await providerContainer.read(tokenContainerProvider.future); + await Future.delayed(const Duration(milliseconds: 1000)); // wait for the sync to finish + tokenState = providerContainer.read(tokenProvider); + verify(mockContainerRepo.loadContainerState()).called(1); + expect(containerState, containerRepoState); + final stateContainer = containerState.containerList.first as TokenContainerFinalized; + final expectedContainer = containerToSync.copyWith( + policies: ContainerPolicies( rolloverAllowed: true, initialTokenTransfer: true, tokensDeletable: true, unregisterAllowed: true, ), - ), - ); - - final mockContainerRepo = _setupMockContainerRepo(() => containerRepoState, (state) => containerRepoState = state); - - final mockTokenContainerProvider = TokenContainerNotifier( - repoOverride: mockContainerRepo, - containerApiOverride: mockContainerApi, - eccUtilsOverride: EccUtils(), - ); - // prepare - token notifier - var repoTokens = { - 'ID01': HOTPToken( - id: 'ID01', - serial: "HOTPTOKEN01", - containerSerial: "CONTAINER01", - algorithm: Algorithms.SHA256, - digits: 8, - secret: "SECRET01", - counter: 10, - ), - "ID02": HOTPToken( - id: "ID02", - serial: "HOTPTOKEN02", - containerSerial: null, - algorithm: Algorithms.SHA256, - digits: 6, - secret: "SECRET02", - counter: 12, - ), - "ID04": TOTPToken( - id: "ID04", - serial: "TOTPTOKEN02", - period: 30, - algorithm: Algorithms.SHA512, - digits: 6, - secret: "SECRET04", - ), - }; - final mockTokenRepo = MockTokenRepository(); - when(mockTokenRepo.loadTokens()).thenAnswer((_) => Future.value(repoTokens.values.toList())); - when(mockTokenRepo.saveOrReplaceTokens(any)).thenAnswer((invocation) { - final tokens = invocation.positionalArguments[0] as List; - for (final token in tokens) { - repoTokens[token.id] = token; - } - return Future.value([]); + ); + verify(mockContainerApi.sync(any, any)).called(1); + expect(stateContainer.policies, expectedContainer.policies); + expect(stateContainer.syncState, SyncState.completed); + expect(tokenState.tokens.length, 3); + expect(tokenState.tokens, unorderedEquals(expectedStateUnordered.tokens)); }); - - final mockTokenNotifier = TokenNotifier( - repoOverride: mockTokenRepo, - ); - - // prepare - settings notifier - final MockSettingsRepository mockSettingsRepo = MockSettingsRepository(); - when(mockSettingsRepo.loadSettings()).thenAnswer((_) => Future.value(SettingsState())); - when(mockSettingsRepo.saveSettings(any)).thenAnswer((invocation) => Future.value(invocation.positionalArguments[0])); - final SettingsNotifier settingsNotifier = SettingsNotifier(repoOverride: mockSettingsRepo); - - // prepare - provider container - final providerContainer = ProviderContainer( - overrides: [ - tokenContainerProvider.overrideWith(() => mockTokenContainerProvider), - tokenProvider.overrideWith(() => mockTokenNotifier), - settingsProvider.overrideWith(() => settingsNotifier), - ], - ); - - // act - var tokenState = providerContainer.read(tokenProvider); - await providerContainer.read(tokenContainerProvider.notifier).sync(tokenState: tokenState, isManually: false); - - // assert - final expectedStateUnordered = TokenState(tokens: [...updatedTokens, repoTokens["ID04"]!]); - final containerState = await providerContainer.read(tokenContainerProvider.future); - await Future.delayed(const Duration(milliseconds: 1000)); // wait for the sync to finish - tokenState = providerContainer.read(tokenProvider); - verify(mockContainerRepo.loadContainerState()).called(1); - expect(containerState, containerRepoState); - final stateContainer = containerState.containerList.first as TokenContainerFinalized; - final expectedContainer = containerToSync.copyWith( - policies: ContainerPolicies( - rolloverAllowed: true, - initialTokenTransfer: true, - tokensDeletable: true, - unregisterAllowed: true, - ), - ); - verify(mockContainerApi.sync(any, any)).called(1); - expect(stateContainer.policies, expectedContainer.policies); - expect(stateContainer.syncState, SyncState.completed); - expect(tokenState.tokens.length, 3); - expect(tokenState.tokens, unorderedEquals(expectedStateUnordered.tokens)); }); test('getRolloverQrData', () async { // prepare diff --git a/test/unit_test/state_notifiers/token_notifier_test.dart b/test/unit_test/state_notifiers/token_notifier_test.dart index 1b93ea6f..c74c21ca 100644 --- a/test/unit_test/state_notifiers/token_notifier_test.dart +++ b/test/unit_test/state_notifiers/token_notifier_test.dart @@ -12,10 +12,10 @@ import 'package:privacyidea_authenticator/model/riverpod_states/settings_state.d import 'package:privacyidea_authenticator/model/tokens/hotp_token.dart'; import 'package:privacyidea_authenticator/model/tokens/push_token.dart'; import 'package:privacyidea_authenticator/model/tokens/token.dart'; -import 'package:privacyidea_authenticator/utils/privacyidea_io_client.dart'; -import 'package:privacyidea_authenticator/utils/riverpod/riverpod_providers/generated_providers/token_notifier.dart'; import 'package:privacyidea_authenticator/utils/logger.dart'; +import 'package:privacyidea_authenticator/utils/privacyidea_io_client.dart'; import 'package:privacyidea_authenticator/utils/riverpod/riverpod_providers/generated_providers/settings_notifier.dart'; +import 'package:privacyidea_authenticator/utils/riverpod/riverpod_providers/generated_providers/token_notifier.dart'; import 'package:privacyidea_authenticator/utils/rsa_utils.dart'; import 'package:privacyidea_authenticator/utils/utils.dart'; @@ -145,7 +145,7 @@ void _testTokenNotifier() { verify(mockRepo.deleteToken(before.last)).called(1); }); group('addOrReplaceToken', () { - test('add Token', () async { + test('add new Token', () async { final mockSettingsRepo = MockSettingsRepository(); when(mockSettingsRepo.loadSettings()).thenAnswer((_) async => SettingsState()); final container = ProviderContainer(overrides: [settingsProvider.overrideWith(() => SettingsNotifier(repoOverride: mockSettingsRepo))]);