001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.juneau.rest.server.mcp.v20260728; 018 019 020import javax.crypto.SecretKey; 021import static org.apache.juneau.commons.utils.Shorts.*; 022 023/** 024 * A key paired with the identifier an operator chose for it, as returned by {@link KeyProvider#currentKey()} 025 * and resolved by {@link KeyProvider#resolveKey(String)}. 026 * 027 * <p> 028 * <b>Never log {@code key}.</b> Mirrors {@link RequestStateCodec}'s "never log" contract for the sealed token: 029 * this record holds the actual sealing key, so {@link #toString()} deliberately redacts {@code key} rather than 030 * relying on the record's auto-generated form, which would print whatever a custom/HSM {@link SecretKey} impl's 031 * own {@code toString()} legally chooses to include. 032 * 033 * @param keyId An opaque, operator-chosen short identifier for {@code key} (e.g. {@code "2026-08-a"}). Must not 034 * be <jk>null</jk> or blank, and is length-bounded so a sealed token's cleartext {@code keyId} wire segment 035 * (see {@code AeadRequestStateCodec}'s versioned wire format) stays bounded. 036 * @param key The key material. Must not be <jk>null</jk>. The AEAD codec is AES-GCM-specific by design, so 037 * non-AES key material fails at cipher-init time in {@code AeadRequestStateCodec}, not here. 038 * @since 10.0.0 039 */ 040public record KeyedSecret(String keyId, SecretKey key) { 041 042 // Bounds the base64url'd keyId wire segment so an oversized keyId cannot inflate a sealed token. 043 private static final int MAX_KEY_ID_CHARS = 128; 044 045 /** 046 * Compact constructor — validates {@code keyId} is non-null/non-blank/length-bounded and {@code key} 047 * is non-null. 048 */ 049 public KeyedSecret { 050 reqnb("keyId", keyId); 051 req(keyId.length() <= MAX_KEY_ID_CHARS, "Argument 'keyId' length (%s) exceeds max of %s chars.", keyId.length(), MAX_KEY_ID_CHARS); 052 reqnn("key", key); 053 } 054 055 /** 056 * Redacts {@code key} so key material never reaches logs via this record's {@code toString()}. 057 */ 058 @Override 059 public String toString() { 060 return "KeyedSecret[keyId=" + keyId + ", key=<redacted>]"; 061 } 062}