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}