From 2e276f08b53a33d67817b4299af4b310138b3f1d Mon Sep 17 00:00:00 2001 From: Andy Date: Mon, 21 Aug 2023 21:46:20 +0200 Subject: [PATCH] 1080: added test for administrtion card hash with startDate, added test for card info utils isCardNotYetValid, add a check that expiry date is after start day --- .../src/bp-modules/cards/AddCardForm.tsx | 23 ++++++++++-- .../cards/extensions/StartDayExtension.tsx | 19 +++------- administration/src/cards/hashCardInfo.test.ts | 23 ++++++++++++ .../backend/verification/database/Schema.kt | 1 + .../verification/service/CardVerifier.kt | 4 +- .../webservice/schema/CardMutationService.kt | 2 + frontend/test/canonical_json_test.dart | 21 +++++++++++ frontend/test/card_info_util_test.dart | 37 +++++++++++++++++++ 8 files changed, 111 insertions(+), 19 deletions(-) diff --git a/administration/src/bp-modules/cards/AddCardForm.tsx b/administration/src/bp-modules/cards/AddCardForm.tsx index 280bb940c..b43b9053f 100644 --- a/administration/src/bp-modules/cards/AddCardForm.tsx +++ b/administration/src/bp-modules/cards/AddCardForm.tsx @@ -4,6 +4,7 @@ import React, { ChangeEvent } from 'react' import styled from 'styled-components' import { CardBlueprint } from '../../cards/CardBlueprint' +import StartDayExtension from '../../cards/extensions/StartDayExtension' import { ExtensionInstance } from '../../cards/extensions/extensions' import PlainDate from '../../util/PlainDate' @@ -18,6 +19,7 @@ const CardHeader = styled.div` interface ExtensionFormProps { extension: ExtensionInstance onUpdate: () => void + hasFormDependencyError?: boolean } interface CreateCardsFormProps { @@ -26,19 +28,27 @@ interface CreateCardsFormProps { onRemove: () => void } -const ExtensionForm = ({ extension, onUpdate }: ExtensionFormProps) => { +const ExtensionForm = ({ extension, onUpdate, hasFormDependencyError }: ExtensionFormProps) => { return extension.createForm(() => { onUpdate() - }) + }, hasFormDependencyError) } const maxCardValidity = { years: 99 } const hasCardExpirationError = (expirationDate: PlainDate): boolean => { const today = PlainDate.fromLocalDate(new Date()) - return expirationDate.isBefore(today) || expirationDate.isAfter(today.add(maxCardValidity)) } +const hasStartAfterExpiryDateError = (expirationDate: PlainDate, extensions: ExtensionInstance[]): boolean => { + const startDayExtension = extensions.find(el => el.name === 'StartDayExtension') as StartDayExtension | undefined + if (startDayExtension?.state?.startDay) { + const startDay = PlainDate.fromDaysSinceEpoch(startDayExtension.state.startDay) + return startDay.isAfter(expirationDate) + } + return false +} + const CreateCardForm = ({ cardBlueprint, onRemove, onUpdate }: CreateCardsFormProps) => { const today = PlainDate.fromLocalDate(new Date()) return ( @@ -87,7 +97,12 @@ const CreateCardForm = ({ cardBlueprint, onRemove, onUpdate }: CreateCardsFormPr /> {cardBlueprint.extensions.map((ext, i) => ( - + ))} ) diff --git a/administration/src/cards/extensions/StartDayExtension.tsx b/administration/src/cards/extensions/StartDayExtension.tsx index 050d35aed..4ff5b5044 100644 --- a/administration/src/cards/extensions/StartDayExtension.tsx +++ b/administration/src/cards/extensions/StartDayExtension.tsx @@ -16,18 +16,11 @@ class StartDayExtension extends Extension { this.state = { startDay: today.toDaysSinceEpoch() } } - hasValidStartDayDate(startDay?: number): boolean { - if (startDay === undefined) { - return false - } - const date = PlainDate.fromDaysSinceEpoch(startDay) - const today = PlainDate.fromLocalDate(new Date()) - return !date.isBefore(today) - } - - createForm(onUpdate: () => void) { + createForm(onUpdate: () => void, hasFormDependencyError?: boolean) { const startDayDate = - this.state?.startDay !== undefined ? PlainDate.fromDaysSinceEpoch(this.state.startDay) : PlainDate.fromLocalDate(new Date()) + this.state?.startDay !== undefined + ? PlainDate.fromDaysSinceEpoch(this.state.startDay) + : PlainDate.fromLocalDate(new Date()) return ( @@ -36,7 +29,7 @@ class StartDayExtension extends Extension { type='date' required size='small' - error={!this.isValid()} + error={!this.isValid() || hasFormDependencyError} value={startDayDate.toString()} sx={{ '& input[value=""]:not(:focus)': { color: 'transparent' }, '& fieldset': { borderRadius: 0 } }} inputProps={{ @@ -69,7 +62,7 @@ class StartDayExtension extends Extension { } isValid() { - return this.state !== null && this.hasValidStartDayDate(this.state.startDay) + return this.state?.startDay !== undefined } /** diff --git a/administration/src/cards/hashCardInfo.test.ts b/administration/src/cards/hashCardInfo.test.ts index dff3bd177..772feb220 100644 --- a/administration/src/cards/hashCardInfo.test.ts +++ b/administration/src/cards/hashCardInfo.test.ts @@ -193,4 +193,27 @@ describe('hashCardInfo', () => { expect(uint8ArrayToBase64(hash)).toBe('zogEJOhnSSp//8qhym/DdorQYgL/763Kfq4slWduxMg=') }) + + it('should be stable for a Nuernberg Pass with startDay', async () => { + const cardInfo = new CardInfo({ + fullName: 'Max Mustermann', + expirationDay: 365 * 40, // Equals 14.600 + extensions: new CardExtensions({ + extensionRegion: new RegionExtension({ + regionId: 93, + }), + extensionBirthday: new BirthdayExtension({ + birthday: -365 * 10, + }), + extensionNuernbergPassNumber: new NuernbergPassNumberExtension({ + passNumber: 99999999, + }), + extensionStartDay: new StartDayExtension({ startDay: 365 * 2 }), + }), + }) + const pepper = base64ToUint8Array('MvMjEqa0ulFDAgACElMjWA==') + const hash = await hashCardInfo(cardInfo, pepper) + + expect(uint8ArrayToBase64(hash)).toBe('1ChHiAvWygwu+bH2yOZOk1zdmwTDZ4mkvu079cyuLjE=') + }) }) diff --git a/backend/src/main/kotlin/app/ehrenamtskarte/backend/verification/database/Schema.kt b/backend/src/main/kotlin/app/ehrenamtskarte/backend/verification/database/Schema.kt index e0598f190..a3638e935 100644 --- a/backend/src/main/kotlin/app/ehrenamtskarte/backend/verification/database/Schema.kt +++ b/backend/src/main/kotlin/app/ehrenamtskarte/backend/verification/database/Schema.kt @@ -36,6 +36,7 @@ object Cards : IntIdTable() { val cardInfoHash = binary("cardInfoHash", CARD_INFO_HASH_LENGTH).uniqueIndex() val codeType = enumeration("codeType", CodeType::class) val firstActivationDate = timestamp("firstActivationDate").nullable() + // startDay describes the first day on which the card is valid. // If this field is null, the card is valid until `expirationDay` without explicitly stating when the validity period started. // Days since 1970-01-01. For more information refer to the card.proto, diff --git a/backend/src/main/kotlin/app/ehrenamtskarte/backend/verification/service/CardVerifier.kt b/backend/src/main/kotlin/app/ehrenamtskarte/backend/verification/service/CardVerifier.kt index 6ee646677..d8385dc28 100644 --- a/backend/src/main/kotlin/app/ehrenamtskarte/backend/verification/service/CardVerifier.kt +++ b/backend/src/main/kotlin/app/ehrenamtskarte/backend/verification/service/CardVerifier.kt @@ -18,13 +18,13 @@ object CardVerifier { public fun verifyStaticCard(project: String, cardHash: ByteArray, timezone: ZoneId): Boolean { val card = transaction { CardRepository.findByHash(project, cardHash) } ?: return false return !isExpired(card.expirationDay, timezone) && isYetValid(card.startDay, timezone) && - !card.revoked + !card.revoked } public fun verifyDynamicCard(project: String, cardHash: ByteArray, totp: Int, timezone: ZoneId): Boolean { val card = transaction { CardRepository.findByHash(project, cardHash) } ?: return false return !isExpired(card.expirationDay, timezone) && isYetValid(card.startDay, timezone) && - !card.revoked && + !card.revoked && isTotpValid(totp, card.totpSecret) } diff --git a/backend/src/main/kotlin/app/ehrenamtskarte/backend/verification/webservice/schema/CardMutationService.kt b/backend/src/main/kotlin/app/ehrenamtskarte/backend/verification/webservice/schema/CardMutationService.kt index 157c82ac5..1e0acb429 100644 --- a/backend/src/main/kotlin/app/ehrenamtskarte/backend/verification/webservice/schema/CardMutationService.kt +++ b/backend/src/main/kotlin/app/ehrenamtskarte/backend/verification/webservice/schema/CardMutationService.kt @@ -86,6 +86,7 @@ class CardMutationService { val activationSecretHash = card?.activationSecretHash if (card == null || activationSecretHash == null) { + logger.info("${context.remoteIp} failed to activate card, card not found with cardHash:$cardHash") return@t CardActivationResultModel(ActivationState.failed) } @@ -95,6 +96,7 @@ class CardMutationService { } if (CardVerifier.isExpired(card.expirationDay, projectConfig.timezone) || card.revoked) { + logger.info("${context.remoteIp} failed to activate card with id:${card.id} and overwrite: $overwrite because card isExpired or revoked") return@t CardActivationResultModel(ActivationState.failed) } diff --git a/frontend/test/canonical_json_test.dart b/frontend/test/canonical_json_test.dart index df5f8f533..13fe3222a 100644 --- a/frontend/test/canonical_json_test.dart +++ b/frontend/test/canonical_json_test.dart @@ -62,5 +62,26 @@ void main() { }, }); }); + + test("should map a cardInfo for a Nuernberg Pass wit startDay correctly", () { + final cardInfo = CardInfo() + ..fullName = 'Max Mustermann' + ..expirationDay = 365 * 40 // Equals 14.600 + ..extensions = (CardExtensions() + ..extensionRegion = (RegionExtension()..regionId = 93) + ..extensionBirthday = (BirthdayExtension()..birthday = -365 * 10) + ..extensionNuernbergPassNumber = (NuernbergPassNumberExtension()..passNumber = 99999999) + ..extensionStartDay = (StartDayExtension()..startDay = 365 * 2)); + expect(cardInfo.toCanonicalJsonObject(), { + '1': 'Max Mustermann', + '2': '14600', + '3': { + '1': {'1': '93'}, // extensionRegion + '2': {'1': '-3650'}, // extensionBirthday + '3': {'1': '99999999'}, // extensionNuernbergPassNumber + '5': {'1': '730'} // startDay extension + }, + }); + }); }); } diff --git a/frontend/test/card_info_util_test.dart b/frontend/test/card_info_util_test.dart index 68d17f2d3..789170085 100644 --- a/frontend/test/card_info_util_test.dart +++ b/frontend/test/card_info_util_test.dart @@ -55,4 +55,41 @@ void main() { expect(cardInfo.hash(pepper), '1ChHiAvWygwu+bH2yOZOk1zdmwTDZ4mkvu079cyuLjE='); }); }); + + group("isCardNotYetValid", () { + test("should return true if startDay is in the future", () { + final cardInfo = CardInfo() + ..fullName = 'Max Mustermann' + ..expirationDay = 365 * 40 // Equals 14.600 + ..extensions = (CardExtensions() + ..extensionRegion = (RegionExtension()..regionId = 93) + ..extensionBirthday = (BirthdayExtension()..birthday = -365 * 10) + ..extensionNuernbergPassNumber = (NuernbergPassNumberExtension()..passNumber = 99999999) + ..extensionStartDay = (StartDayExtension()..startDay = 365 * 70)); + expect(isCardNotYetValid(cardInfo), true); + }); + + test("should return false if startDay is in the past", () { + final cardInfo = CardInfo() + ..fullName = 'Max Mustermann' + ..expirationDay = 365 * 40 // Equals 14.600 + ..extensions = (CardExtensions() + ..extensionRegion = (RegionExtension()..regionId = 93) + ..extensionBirthday = (BirthdayExtension()..birthday = -365 * 10) + ..extensionNuernbergPassNumber = (NuernbergPassNumberExtension()..passNumber = 99999999) + ..extensionStartDay = (StartDayExtension()..startDay = 365 * 30)); + expect(isCardNotYetValid(cardInfo), false); + }); + + test("should be return false if no startDay was set", () { + final cardInfo = CardInfo() + ..fullName = 'Max Mustermann' + ..expirationDay = 365 * 40 // Equals 14.600 + ..extensions = (CardExtensions() + ..extensionRegion = (RegionExtension()..regionId = 93) + ..extensionBirthday = (BirthdayExtension()..birthday = -365 * 10) + ..extensionNuernbergPassNumber = (NuernbergPassNumberExtension()..passNumber = 99999999)); + expect(isCardNotYetValid(cardInfo), false); + }); + }); }