From 6004e3f9eb8d020450956701fd83f03cbc50ac86 Mon Sep 17 00:00:00 2001 From: Artur T <81235055+a-trzewik@users.noreply.github.com> Date: Thu, 20 May 2021 10:06:18 +0200 Subject: [PATCH] checksum with luhn mod n (#63) --- .../dgc/issuance/service/DgciGenerator.java | 69 ++++++++++++++++--- src/main/resources/application.yml | 2 +- .../issuance/service/DgciGeneratorTest.java | 3 +- src/test/resources/application.yml | 2 +- 4 files changed, 62 insertions(+), 14 deletions(-) diff --git a/src/main/java/eu/europa/ec/dgc/issuance/service/DgciGenerator.java b/src/main/java/eu/europa/ec/dgc/issuance/service/DgciGenerator.java index 7dc90cb..8f0f005 100644 --- a/src/main/java/eu/europa/ec/dgc/issuance/service/DgciGenerator.java +++ b/src/main/java/eu/europa/ec/dgc/issuance/service/DgciGenerator.java @@ -22,9 +22,8 @@ import eu.europa.ec.dgc.issuance.config.IssuanceConfigProperties; import eu.europa.ec.dgc.issuance.utils.DgciUtil; -import java.math.BigInteger; -import java.nio.charset.StandardCharsets; import java.util.UUID; +import javax.annotation.PostConstruct; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Component; @@ -33,6 +32,25 @@ public class DgciGenerator { private final IssuanceConfigProperties issuanceConfigProperties; + private static final String CODE_POINTS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789/:"; + + /** + * Check if dgci prefix contains character suitable for checksum calculation. + */ + @PostConstruct + public void checkPrefix() { + String dgciPrefix = issuanceConfigProperties.getDgciPrefix(); + if (dgciPrefix != null) { + for (int i = 0;i < dgciPrefix.length();i++) { + if (CODE_POINTS.indexOf(dgciPrefix.charAt(i)) < 0) { + throw new IllegalArgumentException("configured DGCI prefix '" + + dgciPrefix + "' contains invalid character '" + + dgciPrefix.charAt(i) + "' only following are supported " + CODE_POINTS); + } + } + } + } + /** * Generates a new DGCI. * @@ -42,18 +60,47 @@ public String newDgci() { StringBuilder sb = new StringBuilder(); sb.append(issuanceConfigProperties.getDgciPrefix()).append(':'); sb.append(DgciUtil.encodeDgci(UUID.randomUUID())); - String checkSum = createDgciCheckSum(sb.toString()); - sb.append(':').append(checkSum); + sb.append(generateCheckCharacter(sb.toString())); return sb.toString(); } - private String createDgciCheckSum(String dgciRaw) { - BigInteger dgciRawAsNumber = new BigInteger(1, dgciRaw.getBytes(StandardCharsets.UTF_8)); - BigInteger modValue = dgciRawAsNumber.mod(BigInteger.valueOf(97)); - String checkSum = modValue.toString(); - if (checkSum.length() == 1) { - checkSum = '0' + checkSum; + // see https://en.wikipedia.org/wiki/Luhn_mod_N_algorithm + private char generateCheckCharacter(String input) { + int factor = 2; + int sum = 0; + int n = CODE_POINTS.length(); + + // Starting from the right and working leftwards is easier since + // the initial "factor" will always be "2". + for (int i = input.length() - 1; i >= 0; i--) { + int codePoint = codePointFromCharacter(input.charAt(i)); + int addend = factor * codePoint; + + // Alternate the "factor" that each "codePoint" is multiplied by + factor = (factor == 2) ? 1 : 2; + + // Sum the digits of the "addend" as expressed in base "n" + addend = (addend / n) + (addend % n); + sum += addend; + } + + // Calculate the number that must be added to the "sum" + // to make it divisible by "n". + int remainder = sum % n; + int checkCodePoint = (n - remainder) % n; + + return characterFromCodePoint(checkCodePoint); + } + + private char characterFromCodePoint(int checkCodePoint) { + return CODE_POINTS.charAt(checkCodePoint); + } + + private int codePointFromCharacter(char charAt) { + int codePoint = CODE_POINTS.indexOf(charAt); + if (codePoint < 0) { + throw new IllegalArgumentException("unsupported character for checksum: " + charAt); } - return checkSum; + return codePoint; } } diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index a463b07..0341950 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -40,7 +40,7 @@ springdoc: swagger-ui: path: /swagger issuance: - dgciPrefix: dgci:V1:DE + dgciPrefix: URN:UVCI:V1:DE keyStoreFile: certs/test.jks keyStorePassword: dgca certAlias: dev_ec diff --git a/src/test/java/eu/europa/ec/dgc/issuance/service/DgciGeneratorTest.java b/src/test/java/eu/europa/ec/dgc/issuance/service/DgciGeneratorTest.java index 61abc13..943f163 100644 --- a/src/test/java/eu/europa/ec/dgc/issuance/service/DgciGeneratorTest.java +++ b/src/test/java/eu/europa/ec/dgc/issuance/service/DgciGeneratorTest.java @@ -30,11 +30,12 @@ public class DgciGeneratorTest { @Test public void testGenerateDGCI() throws Exception { IssuanceConfigProperties issuanceConfigProperties = new IssuanceConfigProperties(); - issuanceConfigProperties.setDgciPrefix("dgci:V1:DE"); + issuanceConfigProperties.setDgciPrefix("URN:UVCI:V1:DE"); DgciGenerator dgciGenerator = new DgciGenerator(issuanceConfigProperties); String dgci = dgciGenerator.newDgci(); assertNotNull(dgci); assertTrue(dgci.startsWith(issuanceConfigProperties.getDgciPrefix())); + assertTrue("dgci too long",dgci.length() <= 50); System.out.println(dgci); } } diff --git a/src/test/resources/application.yml b/src/test/resources/application.yml index 0bed214..647a95e 100644 --- a/src/test/resources/application.yml +++ b/src/test/resources/application.yml @@ -11,7 +11,7 @@ spring: main: allow-bean-definition-overriding: true issuance: - dgciPrefix: dgci:V1:DE + dgciPrefix: URN:UVCI:V1:DE keyStoreFile: certs/test.jks keyStorePassword: dgca certAlias: edgc_dev_ec