001/* 002 * Copyright 2022-2026 Revetware LLC. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016 017package com.soklet; 018 019import org.jspecify.annotations.NonNull; 020 021import javax.annotation.concurrent.ThreadSafe; 022import java.security.MessageDigest; 023import java.util.Arrays; 024 025import static java.util.Objects.requireNonNull; 026 027/** 028 * Immutable, purpose-specific MCP request-state protection master key. 029 * <p> 030 * Supplied bytes are defensively copied and are never exposed through the 031 * public API, diagnostic rendering, logs, metrics, fingerprints, or exception 032 * messages. Applications remain responsible for generating key material with 033 * a cryptographically secure random source and at least 256 bits of entropy. 034 * 035 * <p>Instances intentionally retain reference identity so secret key material 036 * never participates in public equality or hash-code operations. Use 037 * {@link McpProtectionKeyringSnapshot#getFingerprint()} when comparing 038 * complete non-secret keyring configurations across instances. 039 * 040 * @author <a href="https://www.revetkn.com">Mark Allen</a> 041 */ 042@ThreadSafe 043public final class McpProtectionKey { 044 private static final int MINIMUM_KEY_BYTES = 32; 045 @NonNull 046 private final String keyId; 047 private final byte @NonNull [] keyMaterial; 048 049 /** 050 * Creates a protection master key from exact bytes. 051 * 052 * @param keyId 1-64-byte ASCII HTTP token 053 * @param keyMaterial at least 32 bytes of operator-generated key material 054 * @return immutable key value 055 * @throws IllegalArgumentException if the ID or material is invalid 056 */ 057 @NonNull 058 public static McpProtectionKey fromIdAndBytes(@NonNull String keyId, 059 byte @NonNull [] keyMaterial) { 060 return new McpProtectionKey(keyId, keyMaterial); 061 } 062 063 private McpProtectionKey(@NonNull String keyId, 064 byte @NonNull [] keyMaterial) { 065 this.keyId = McpKeyIdValidator.validate(keyId, 066 "MCP protection key ID"); 067 requireNonNull(keyMaterial); 068 if (keyMaterial.length < MINIMUM_KEY_BYTES) 069 throw new IllegalArgumentException( 070 "MCP protection keys must contain at least 32 bytes."); 071 this.keyMaterial = keyMaterial.clone(); 072 } 073 074 /** @return non-secret key ID */ 075 @NonNull 076 public String getKeyId() { 077 return this.keyId; 078 } 079 080 byte @NonNull [] copyKeyMaterial() { 081 return this.keyMaterial.clone(); 082 } 083 084 boolean hasSameMaterial(@NonNull McpProtectionKey other) { 085 return MessageDigest.isEqual(this.keyMaterial, 086 requireNonNull(other).keyMaterial); 087 } 088 089 boolean hasSameMaterial(@NonNull McpTraceCorrelationKey other) { 090 byte[] otherKeyMaterial = requireNonNull(other).copyKeyMaterial(); 091 try { 092 return MessageDigest.isEqual(this.keyMaterial, otherKeyMaterial); 093 } finally { 094 Arrays.fill(otherKeyMaterial, (byte) 0); 095 } 096 } 097 098 /** @return redacted rendering containing only the non-secret key ID */ 099 @Override 100 @NonNull 101 public String toString() { 102 return "%s{keyId='%s', keyMaterial=<redacted>}" 103 .formatted(getClass().getSimpleName(), this.keyId); 104 } 105}