Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.unomi.api.security;

/**
* One-way hashing for secrets that must never be stored in plaintext. This is distinct from
* {@link EncryptionService}, which handles reversible encryption keys.
* <p>
* API keys are machine-generated with high entropy and hashed with SHA-256 (lowercase hex) for
* storage and online verification.
*/
public interface SecretHashService {

/**
* Hashes a secret with SHA-256 for storage.
*
* @param plaintext the secret to hash; must not be {@code null}
* @return lowercase hex-encoded SHA-256 digest
* @throws IllegalArgumentException if {@code plaintext} is {@code null}
*/
String hash(String plaintext);

/**
* Verifies a secret against a stored SHA-256 digest using a constant-time comparison.
*
* @param plaintext the plaintext secret to verify
* @param storedHash the stored digest, as produced by {@link #hash(String)}
* @return {@code true} if the secret matches, {@code false} otherwise
*/
boolean verify(String plaintext, String storedHash);
}
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,27 @@ public interface SecurityService {
*/
void clearCurrentSubject();

/**
* Returns exactly the subject bound by {@link #setCurrentSubject(Subject)}, ignoring any
* active JAAS context or privileged subject override. Unlike {@link #getCurrentSubject()},
* this never resolves to a higher-priority subject, so callers that need to snapshot and
* later restore only the request-subject slot (e.g. {@code executeAsSystem}) don't
* accidentally capture a privileged subject and leak it into the request-subject slot on
* restore.
*
* @return the request subject currently bound via {@link #setCurrentSubject(Subject)}, or
* {@code null} if none is bound
*/
Subject getRequestSubject();

/**
* Clears only the request-subject slot set by {@link #setCurrentSubject(Subject)}, without
* affecting any active privileged subject (unlike {@link #clearCurrentSubject()}, which
* clears both). Used to restore that slot to "unset" after a save/restore scope such as
* {@code executeAsSystem}.
*/
void clearRequestSubject();

/**
* Checks if the current context has a specific role by examining subjects in the following order:
* 1. JAAS context
Expand Down
87 changes: 77 additions & 10 deletions api/src/main/java/org/apache/unomi/api/tenants/ApiKey.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

import org.apache.unomi.api.Item;

import java.security.SecureRandom;
import java.util.Date;

/**
Expand All @@ -31,6 +32,17 @@ public class ApiKey extends Item {
*/
public static final String ITEM_TYPE = "apiKey";

/** Prefix prepended to generated API key values. */
public static final String KEY_PREFIX = "unomi_v1_";

/** Number of random bytes used when generating a new API key. */
public static final int KEY_RANDOM_BYTES = 32;

/** Number of trailing characters shown after the mask marker. */
private static final int VISIBLE_SUFFIX_LENGTH = 4;

private static final SecureRandom SECURE_RANDOM = new SecureRandom();

/**
* Enum defining the types of API keys.
*/
Expand All @@ -47,9 +59,16 @@ public enum ApiKeyType {
}

/**
* The API key value.
* SHA-256 hex digest of the API key ({@link org.apache.unomi.api.security.SecretHashService#hash(String)}).
* The plaintext key is never persisted; it is only returned once at creation time.
*/
private String keyHash;

/**
* A display-safe, masked representation of the key (e.g. "unomi_v1_****ab12"),
* suitable for showing in UIs and logs without exposing the secret.
*/
private String key;
private String maskedKey;

/**
* The type of API key (public or private).
Expand Down Expand Up @@ -90,19 +109,67 @@ public ApiKey() {
}

/**
* Gets the API key value.
* @return the API key value
* Generates a new plaintext API key.
*
* @return a newly generated plaintext API key with the {@link #KEY_PREFIX} prefix
*/
public static String generatePlainTextKey() {
byte[] randomBytes = new byte[KEY_RANDOM_BYTES];
SECURE_RANDOM.nextBytes(randomBytes);
StringBuilder hex = new StringBuilder(randomBytes.length * 2);
for (byte b : randomBytes) {
hex.append(String.format("%02X", b));
}
return KEY_PREFIX + hex;
}

/**
* Produces a display-safe masked representation of a plaintext API key.
*
* @param plainTextKey the plaintext API key to mask; may be {@code null}
* @return the masked key (e.g. {@code unomi_v1_****ab12}), or {@code null} when {@code plainTextKey} is {@code null}
*/
public static String maskPlainTextKey(String plainTextKey) {
if (plainTextKey == null) {
return null;
}
boolean hasPrefix = plainTextKey.startsWith(KEY_PREFIX);
String body = hasPrefix ? plainTextKey.substring(KEY_PREFIX.length()) : plainTextKey;
int suffixLength = Math.min(VISIBLE_SUFFIX_LENGTH, body.length());
String lastVisible = body.substring(body.length() - suffixLength);
return (hasPrefix ? KEY_PREFIX : "") + "****" + lastVisible;
}

/**
* Gets the SHA-256 digest of the API key.
* @return the key hash as lowercase hex
*/
public String getKeyHash() {
return keyHash;
}

/**
* Sets the SHA-256 digest of the API key.
* @param keyHash the key hash to set
*/
public void setKeyHash(String keyHash) {
this.keyHash = keyHash;
}

/**
* Gets the display-safe masked representation of the key.
* @return the masked key (e.g. "unomi_v1_****ab12")
*/
public String getKey() {
return key;
public String getMaskedKey() {
return maskedKey;
}

/**
* Sets the API key value.
* @param key the API key value to set
* Sets the display-safe masked representation of the key.
* @param maskedKey the masked key to set
*/
public void setKey(String key) {
this.key = key;
public void setMaskedKey(String maskedKey) {
this.maskedKey = maskedKey;
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.unomi.api.tenants;

/**
* Result of an API key creation operation.
* Carries the persisted {@link ApiKey} metadata (which only stores a hash and a masked
* representation of the key) together with the one-time plaintext key value. The plaintext
* key is only ever available at creation time; it cannot be recovered afterwards since it
* is not persisted (see UNOMI-938).
*/
public class ApiKeyCreationResult {

private ApiKey apiKey;
private String plainTextKey;

public ApiKeyCreationResult() {
}

public ApiKeyCreationResult(ApiKey apiKey, String plainTextKey) {
this.apiKey = apiKey;
this.plainTextKey = plainTextKey;
}

/**
* Gets the persisted API key metadata (type, masked key, dates, etc.), without the secret.
* @return the API key metadata
*/
public ApiKey getApiKey() {
return apiKey;
}

/**
* Sets the persisted API key metadata.
* @param apiKey the API key metadata to set
*/
public void setApiKey(ApiKey apiKey) {
this.apiKey = apiKey;
}

/**
* Gets the one-time plaintext key value. This is only available right after creation;
* it is never persisted and cannot be retrieved again afterwards.
* @return the plaintext API key
*/
public String getPlainTextKey() {
return plainTextKey;
}

/**
* Sets the one-time plaintext key value.
* @param plainTextKey the plaintext API key to set
*/
public void setPlainTextKey(String plainTextKey) {
this.plainTextKey = plainTextKey;
}
}
26 changes: 14 additions & 12 deletions api/src/main/java/org/apache/unomi/api/tenants/Tenant.java
Original file line number Diff line number Diff line change
Expand Up @@ -269,11 +269,12 @@ public void setAuthorizedIPs(Set<String> authorizedIPs) {
}

/**
* Gets the currently active private API key for the tenant.
* This method resolves the active private API key from the API keys list.
* It returns the most recently created, non-revoked, non-expired private key.
* This key should be used for secure operations and administrative tasks.
* @return the active private API key, or null if no valid private key exists
* Gets a display-safe, masked representation of the tenant's currently active private API
* key (e.g. for showing in UIs or audit logs). It resolves the most recently created,
* non-revoked, non-expired private key, but since the plaintext key is never persisted (see
* UNOMI-938), this cannot be used to authenticate as the tenant; the real plaintext key is
* only available once, at creation time, via {@link ApiKeyCreationResult}.
* @return the masked private API key, or null if no valid private key exists
*/
@XmlTransient
public String getPrivateApiKey() {
Expand All @@ -286,16 +287,17 @@ public String getPrivateApiKey() {
.filter(key -> !key.isRevoked())
.filter(key -> key.getExpirationDate() == null || key.getExpirationDate().after(new Date()))
.max(Comparator.comparing(ApiKey::getCreationDate))
.map(ApiKey::getKey)
.map(ApiKey::getMaskedKey)
.orElse(null);
}

/**
* Gets the currently active public API key for the tenant.
* This method resolves the active public API key from the API keys list.
* It returns the most recently created, non-revoked, non-expired public key.
* This key can be safely used in client-side applications.
* @return the active public API key, or null if no valid public key exists
* Gets a display-safe, masked representation of the tenant's currently active public API
* key (e.g. for showing in UIs or audit logs). It resolves the most recently created,
* non-revoked, non-expired public key, but since the plaintext key is never persisted (see
* UNOMI-938), this cannot be used in client-side applications; the real plaintext key is
* only available once, at creation time, via {@link ApiKeyCreationResult}.
* @return the masked public API key, or null if no valid public key exists
*/
@XmlTransient
public String getPublicApiKey() {
Expand All @@ -308,7 +310,7 @@ public String getPublicApiKey() {
.filter(key -> !key.isRevoked())
.filter(key -> key.getExpirationDate() == null || key.getExpirationDate().after(new Date()))
.max(Comparator.comparing(ApiKey::getCreationDate))
.map(ApiKey::getKey)
.map(ApiKey::getMaskedKey)
.orElse(null);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,13 +45,15 @@ public interface TenantService {

/**
* Generates a new API key for the specified tenant with an optional validity period.
* The plaintext key is only ever available on the returned result; it is not persisted
* and cannot be retrieved again afterwards (see UNOMI-938).
*
* @param tenantId the ID of the tenant for which to generate the API key
* @param validityPeriod the period (in milliseconds) for which the API key should be valid, null for no expiration
* @return the generated ApiKey object containing the key and associated metadata
* @return the generated key metadata together with its one-time plaintext value
* @throws IllegalArgumentException if tenantId is null or does not exist
*/
ApiKey generateApiKey(String tenantId, Long validityPeriod);
ApiKeyCreationResult generateApiKey(String tenantId, Long validityPeriod);

/**
* Retrieves a tenant by its ID.
Expand Down Expand Up @@ -97,14 +99,16 @@ public interface TenantService {

/**
* Generates a new API key of the specified type for the tenant.
* The plaintext key is only ever available on the returned result; it is not persisted
* and cannot be retrieved again afterwards (see UNOMI-938).
*
* @param tenantId the ID of the tenant for which to generate the API key
* @param keyType the type of API key to generate (PUBLIC or PRIVATE)
* @param validityPeriod the period (in milliseconds) for which the API key should be valid, null for no expiration
* @return the generated ApiKey object containing the key and associated metadata
* @return the generated key metadata together with its one-time plaintext value
* @throws IllegalArgumentException if tenantId is null or does not exist
*/
ApiKey generateApiKeyWithType(String tenantId, ApiKey.ApiKeyType keyType, Long validityPeriod);
ApiKeyCreationResult generateApiKeyWithType(String tenantId, ApiKey.ApiKeyType keyType, Long validityPeriod);

/**
* Validates an API key for a given tenant and checks if it has the required type.
Expand Down
Loading
Loading