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 java.util.Base64;
021import java.util.Collections;
022import java.util.HashMap;
023import java.util.Map;
024import java.util.Optional;
025
026import javax.crypto.SecretKey;
027import javax.crypto.spec.SecretKeySpec;
028import static org.apache.juneau.commons.utils.Shorts.*;
029
030/**
031 * Built-in, immutable, static-at-startup {@link KeyProvider}: an operator-supplied keyring of one or more AES
032 * keys, one of which is designated the current sealing key. Unlike {@link EphemeralKeyProvider}, the keyring is
033 * fixed at construction time (via {@link Builder} or {@link #of(String, SecretKey)}) rather than generated
034 * randomly, so it can be shared across process instances — the fix for horizontally-scaled MRTR RESUME
035 * (see {@code AeadRequestStateCodec}).
036 *
037 * <h5 class='section'>Example:</h5>
038 * <p class='bjava'>
039 *    KeyProvider <jv>kp</jv> = StaticKeyProvider.<jsm>create</jsm>()
040 *       .addKey(<js>"2026-08-a"</js>, <jv>k1</jv>)
041 *       .current(<js>"2026-08-a"</js>)
042 *       .addKey(<js>"2026-07-z"</js>, <jv>kOld</jv>)
043 *       .build();
044 *
045 *    <jc>// Or, for the common single-shared-key case:</jc>
046 *    KeyProvider <jv>kp2</jv> = StaticKeyProvider.<jsm>of</jsm>(<js>"2026-08-a"</js>, <jv>k1</jv>);
047 * </p>
048 *
049 * @since 10.0.0
050 */
051public final class StaticKeyProvider implements KeyProvider {
052
053   private final KeyedSecret current;
054   private final Map<String,SecretKey> keysById;
055
056   private StaticKeyProvider(KeyedSecret current, Map<String,SecretKey> keysById) {
057      this.current = current;
058      // Defensive copy: keysById may be the Builder's own live map, and the Builder is not consumed by
059      // build() (an operator can keep calling addKey(...)/build() to mint successive keyrings), so this
060      // instance must not observe later mutations of that map.
061      this.keysById = Collections.unmodifiableMap(new HashMap<>(keysById));
062   }
063
064   /**
065    * Creates a new {@link Builder}.
066    *
067    * @return A new builder. Never <jk>null</jk>.
068    */
069   public static Builder create() {
070      return new Builder();
071   }
072
073   /**
074    * One-liner for the common single-shared-key case: a provider with exactly one key, designated current.
075    *
076    * @param keyId The key's identifier. Must not be <jk>null</jk> or blank.
077    * @param key The key material. Must not be <jk>null</jk>.
078    * @return A new, immutable {@link StaticKeyProvider} holding just this one key. Never <jk>null</jk>.
079    * @throws IllegalArgumentException If {@code keyId} is <jk>null</jk>/blank or {@code key} is <jk>null</jk>.
080    */
081   public static StaticKeyProvider of(String keyId, SecretKey key) {
082      return create().addKey(keyId, key).current(keyId).build();
083   }
084
085   /**
086    * Builds an AES {@link SecretKey} from raw key bytes.
087    *
088    * @param value The raw AES key bytes (16/24/32 bytes for AES-128/192/256). Must not be <jk>null</jk>.
089    * @return A new AES {@link SecretKey} wrapping {@code value}. Never <jk>null</jk>.
090    */
091   public static SecretKey aesKey(byte[] value) {
092      reqnn("value", value);
093      return new SecretKeySpec(value, "AES");
094   }
095
096   /**
097    * Builds an AES {@link SecretKey} from base64-encoded key bytes.
098    *
099    * @param base64 The base64-encoded AES key bytes. Must not be <jk>null</jk> or blank.
100    * @return A new AES {@link SecretKey} wrapping the decoded bytes. Never <jk>null</jk>.
101    */
102   public static SecretKey aesKey(String base64) {
103      reqnb("base64", base64);
104      return aesKey(Base64.getDecoder().decode(base64));
105   }
106
107   @Override /* KeyProvider */
108   public KeyedSecret currentKey() {
109      return current;
110   }
111
112   @Override /* KeyProvider */
113   public Optional<SecretKey> resolveKey(String keyId) {
114      return Optional.ofNullable(keysById.get(keyId));
115   }
116
117   /**
118    * Builder for {@link StaticKeyProvider}.
119    */
120   public static final class Builder {
121
122      private final Map<String,SecretKey> keys = new HashMap<>();
123      private String currentKeyId;
124
125      private Builder() {}
126
127      /**
128       * Adds a resolvable key.
129       *
130       * @param keyId The key's identifier. Must not be <jk>null</jk> or blank.
131       * @param key The key material. Must not be <jk>null</jk>.
132       * @return This object.
133       * @throws IllegalArgumentException If {@code keyId} is <jk>null</jk>/blank or {@code key} is
134       *    <jk>null</jk>.
135       */
136      public Builder addKey(String keyId, SecretKey key) {
137         reqnb("keyId", keyId);
138         reqnn("key", key);
139         keys.put(keyId, key);
140         return this;
141      }
142
143      /**
144       * Designates the current sealing key by identifier. Must name a key previously added via
145       * {@link #addKey(String, SecretKey)}.
146       *
147       * @param keyId The identifier of the key to designate as current. Must not be <jk>null</jk> or blank.
148       * @return This object.
149       * @throws IllegalArgumentException If {@code keyId} is <jk>null</jk> or blank.
150       */
151      public Builder current(String keyId) {
152         reqnb("keyId", keyId);
153         currentKeyId = keyId;
154         return this;
155      }
156
157      /**
158       * Builds an immutable {@link StaticKeyProvider} snapshot of this builder's current state.
159       *
160       * @return A new, immutable {@link StaticKeyProvider}. Never <jk>null</jk>.
161       * @throws IllegalArgumentException If no current key was designated via {@link #current(String)}, or
162       *    the designated current key was never added via {@link #addKey(String, SecretKey)}.
163       */
164      public StaticKeyProvider build() {
165         req(currentKeyId != null, "No current key designated; call current(keyId) before build().");
166         var key = keys.get(currentKeyId);
167         req(key != null, "current(''%s'') does not name a key added via addKey(...).", currentKeyId);
168         return new StaticKeyProvider(new KeyedSecret(currentKeyId, key), keys);
169      }
170   }
171}