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 &mdash; 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}