From 14c1e97b0537a98ea5523937b20bdd853b3e5905 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Schauer-K=C3=B6ckeis?= Date: Mon, 14 Oct 2024 14:01:44 +0200 Subject: [PATCH] Api Keys are now stored hashed in DB Only at creation are once returned in plain MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Thomas Schauer-Köckeis --- alpine-infra/pom.xml | 4 +++ .../persistence/AlpineQueryManager.java | 33 +++++++++++++++---- .../src/main/java/alpine/model/ApiKey.java | 20 ++++++++--- 3 files changed, 47 insertions(+), 10 deletions(-) diff --git a/alpine-infra/pom.xml b/alpine-infra/pom.xml index c3f02a85..c39f1c61 100644 --- a/alpine-infra/pom.xml +++ b/alpine-infra/pom.xml @@ -71,6 +71,10 @@ io.jsonwebtoken jjwt + + org.mindrot + jbcrypt + org.junit.jupiter diff --git a/alpine-infra/src/main/java/alpine/persistence/AlpineQueryManager.java b/alpine-infra/src/main/java/alpine/persistence/AlpineQueryManager.java index a4e001ae..e2d5c14a 100644 --- a/alpine-infra/src/main/java/alpine/persistence/AlpineQueryManager.java +++ b/alpine-infra/src/main/java/alpine/persistence/AlpineQueryManager.java @@ -18,6 +18,7 @@ */ package alpine.persistence; +import alpine.Config; import alpine.common.logging.Logger; import alpine.event.LdapSyncEvent; import alpine.event.framework.EventService; @@ -40,8 +41,12 @@ import javax.jdo.PersistenceManager; import javax.jdo.Query; + +import org.mindrot.jbcrypt.BCrypt; + import java.sql.Timestamp; import java.util.ArrayList; +import java.util.Base64; import java.util.Date; import java.util.LinkedHashSet; import java.util.List; @@ -56,6 +61,8 @@ public class AlpineQueryManager extends AbstractAlpineQueryManager { private static final Logger LOGGER = Logger.getLogger(AlpineQueryManager.class); + private static final int ROUNDS = Config.getInstance().getPropertyAsInt(Config.AlpineKey.BCRYPT_ROUNDS); + private static final int SUFFIX_LENGTH = 5; /** * Default constructor. @@ -98,9 +105,10 @@ public AlpineQueryManager(final PersistenceManager pm, final AlpineRequest reque */ public ApiKey getApiKey(final String key) { return callInTransaction(() -> { - final Query query = pm.newQuery(ApiKey.class, "key == :key"); - query.setParameters(key); - return executeAndCloseUnique(query); + final Query query = pm.newQuery(ApiKey.class, "suffix == :suffix"); + query.setParameters(key.substring(key.length() - SUFFIX_LENGTH)); + ApiKey apiKey = executeAndCloseUnique(query); + return BCrypt.checkpw(key.substring(0, key.length() - SUFFIX_LENGTH), apiKey.getKey()) ? apiKey : null; }); } @@ -114,7 +122,13 @@ public ApiKey getApiKey(final String key) { */ public ApiKey regenerateApiKey(final ApiKey apiKey) { return callInTransaction(() -> { - apiKey.setKey(ApiKeyGenerator.generate()); + String clearKey = ApiKeyGenerator.generate(); + String hashedKey = BCrypt.hashpw(new String(Base64.getDecoder().decode(clearKey)), BCrypt.gensalt(ROUNDS)) + .toCharArray().toString(); + apiKey.setKey(hashedKey); + apiKey.setSuffix(ApiKeyGenerator.generate(SUFFIX_LENGTH)); + pm.makeTransient(apiKey); + apiKey.setKey(clearKey + apiKey.getSuffix()); return apiKey; }); } @@ -128,10 +142,17 @@ public ApiKey regenerateApiKey(final ApiKey apiKey) { public ApiKey createApiKey(final Team team) { return callInTransaction(() -> { final var apiKey = new ApiKey(); - apiKey.setKey(ApiKeyGenerator.generate()); + String clearKey = ApiKeyGenerator.generate(); + String hashedKey = BCrypt.hashpw(new String(Base64.getDecoder().decode(clearKey)), BCrypt.gensalt(ROUNDS)) + .toCharArray().toString(); + apiKey.setKey(hashedKey); + apiKey.setSuffix(ApiKeyGenerator.generate(SUFFIX_LENGTH)); apiKey.setCreated(new Date()); apiKey.setTeams(List.of(team)); - return pm.makePersistent(apiKey); + pm.makePersistent(apiKey); + pm.makeTransient(apiKey); + apiKey.setKey(clearKey + apiKey.getSuffix()); + return apiKey; }); } diff --git a/alpine-model/src/main/java/alpine/model/ApiKey.java b/alpine-model/src/main/java/alpine/model/ApiKey.java index fe9e3e3a..8095ece4 100644 --- a/alpine-model/src/main/java/alpine/model/ApiKey.java +++ b/alpine-model/src/main/java/alpine/model/ApiKey.java @@ -64,6 +64,7 @@ public class ApiKey implements Serializable, Principal { @Size(min = 32, max = 255) @Pattern(regexp = RegexSequence.Definition.WORD_CHARS, message = "The API key must contain only alpha, numeric and/or underscore characters") + @JsonIgnore private String key; @Persistent @@ -86,6 +87,11 @@ public class ApiKey implements Serializable, Principal { @JsonIgnore private List teams; + @Persistent + @Unique + @Column(name = "SUFFIX") + private String suffix; + public long getId() { return id; } @@ -115,9 +121,9 @@ public String getMaskedKey() { if (key.startsWith(prefix)) maskedKey.append(prefix); - // mask all characters except the last four - maskedKey.append("*".repeat(key.length() - maskedKey.length() - 4)); - maskedKey.append(key.substring(key.length() - 4)); + // mask all characters except for the suffix + maskedKey.append("*".repeat(key.length() - maskedKey.length() - suffix.length())); + maskedKey.append(suffix); return maskedKey.toString(); } @@ -166,5 +172,11 @@ public void setTeams(List teams) { this.teams = teams; } -} + public String getSuffix() { + return suffix; + } + public void setSuffix(String suffix) { + this.suffix = suffix; + } +}