ISecretAttributeProtector
Namespace: Meshmakers.Octo.Runtime.Contracts.Secrets
Encrypts and decrypts values of Secret attributes with the instance key ring
(AB#5528, concept §3.4, §3.5, §3.7). Implemented in Runtime.Engine and registered by
AddRuntimeEngine() from the configuration section SecretEncryption.
public interface ISecretAttributeProtector
Remarks:
Decryption is a privileged operation: every ISecretAttributeProtector.Unprotect(RtSecretValue, SecretAccessContext)
is counted (octo.secrets.decrypt), values are never logged, and the public APIs
never decrypt (architecture tests in asset repo and MCP allowlist the callers).
Without configured keys the service starts; ISecretAttributeProtector.IsConfigured is false and every operation that needs key material throws SecretEncryptionNotConfiguredException.
Properties
IsConfigured
True when an active key is configured, i.e. ISecretAttributeProtector.Protect(String) can work.
public abstract bool IsConfigured { get; }
Property Value
ActiveKeyId
The key id new values are encrypted with; null when not configured.
public abstract string ActiveKeyId { get; }
Property Value
IsStrictMode
True when strict mode is on (configuration SecretEncryption:StrictMode, concept
decision 10, §5.2 phase 5): legacy clear text is no longer readable through
ISecretAttributeProtector.Unprotect(RtSecretValue, SecretAccessContext) /
ISecretAttributeProtector.Unprotect(RtSecretValue, SecretAccessContext), which then throw
LegacyPlaintextSecretRejectedException. ISecretAttributeProtector.Reprotect(RtSecretValue, SecretAccessContext) still converts it.
Implementations without strict mode return false.
public bool IsStrictMode { get; }
Property Value
IsLegacyV1KeyConfigured
True when the legacy enc:v1 key (configuration SecretEncryption:LegacyV1Key) is
configured, i.e. a legacy enc:v1 string can be decrypted. Without it such a value is
classified like a protected value with an unknown key id (SecretValueState.KeyMissing,
key id SecretValueStates.LegacyV1KeyId) by ISecretAttributeProtector.GetReadState(RtSecretValue, SecretAccessContext),
ISecretAttributeProtector.DescribeSecret(RtSecretValue, SecretAccessContext), the secrets overview and the sweep (AB#5532: a key-free Verify lists it
as a re-entry task). Implementations without that knowledge return true (the value counts as set).
public bool IsLegacyV1KeyConfigured { get; }
Property Value
Methods
IsKnownKeyId(String)
True when keyId names a key of the key ring (case-insensitive), i.e. an
enc:v2 envelope with this key id can be decrypted. The sweep (AB#5532) classifies
values with an unknown key id (decision 5: restored from another environment) with it.
bool IsKnownKeyId(string keyId)
Parameters
keyId String
Returns
Protect(String)
Encrypts a plaintext with the active key: enc:v2:<kid>:..., AES-256-GCM, random
nonce, associated data = the ASCII header. The result's RtSecretValue.SetAt is the
current UTC time (new input).
RtSecretValue Protect(string plaintext)
Parameters
plaintext String
Returns
Exceptions
SecretEncryptionNotConfiguredException
No active key
Unprotect(RtSecretValue, SecretAccessContext)
Returns the plaintext of a secret value: decrypts enc:v2 (key ring) and enc:v1
(legacy key), returns legacy clear text (counted as a plaintext read) and the plaintext of
a pending value. In strict mode (ISecretAttributeProtector.IsStrictMode) legacy clear text is rejected.
A legacy value (a string found in a Secret slot) may be clear text or enc:v1; an
enc:v2 envelope as legacy text is never decrypted (AB#5532: the engine stores enc:v2
only in the protected form, so such a text was copied there).
string Unprotect(RtSecretValue value, SecretAccessContext context)
Parameters
value RtSecretValue
context SecretAccessContext
Returns
Exceptions
LegacyPlaintextSecretRejectedException
Strict mode and the value is legacy clear text
SecretEnvelopeNotAllowedException
A legacy value whose text is an enc:v2 envelope
SecretEncryptionNotConfiguredException
The needed key material is not configured
UnknownSecretKeyIdException
The envelope's key id is not in the key ring
CryptographicException
The value was tampered with or the key is wrong
Unprotect(String, SecretAccessContext)
Returns the plaintext of a stored string: an enc:v2 or enc:v1 envelope is
decrypted, anything else is treated as legacy clear text (counted as a plaintext read; rejected
in strict mode, see ISecretAttributeProtector.IsStrictMode).
string Unprotect(string storedValue, SecretAccessContext context)
Parameters
storedValue String
context SecretAccessContext
Returns
Exceptions
LegacyPlaintextSecretRejectedException
Strict mode and the value is clear text
SecretEncryptionNotConfiguredException
The needed key material is not configured
UnknownSecretKeyIdException
The envelope's key id is not in the key ring
CryptographicException
The value was tampered with or the key is wrong
Remarks:
This overload is the explicit envelope entry point (a caller that holds an envelope it trusts,
e.g. InstanceSecretCrypto) and still decrypts enc:v2. Values read from a Secret
slot go through ISecretAttributeProtector.Unprotect(RtSecretValue, SecretAccessContext), which refuses an
enc:v2 envelope stored as a legacy string; never pass the text of a
RtSecretValueState.LegacyPlaintext value here.
GetReadState(RtSecretValue, SecretAccessContext)
Classifies a value for readers (decisions 2026-10-06, item 2) without decrypting it:
SecretValueState.Set (protected with a known key id, non-empty legacy or pending),
SecretValueState.KeyMissing (protected, key id not in the ring - the ciphertext is
kept and becomes readable once the key is added) or SecretValueState.NotSet
(null, empty, a legacy placeholder, or corrupt - see SecretValueStates.IsCorrupt(RtSecretValue)).
APIs map it to isSet = (state == Set) and keyMissing = (state == KeyMissing).
SecretValueState GetReadState(RtSecretValue value, SecretAccessContext context)
Parameters
value RtSecretValue
The stored value; null = not set
context SecretAccessContext
Where the value comes from (log and counter tags only)
Returns
SecretValueState
The read state
Remarks:
The default implementation is SecretValueStates.GetReadState(RtSecretValue, Func<String, Boolean>)
with ISecretAttributeProtector.IsKnownKeyId(String) and ISecretAttributeProtector.IsLegacyV1KeyConfigured (a legacy enc:v1 string
without the legacy key is SecretValueState.KeyMissing); the engine implementation additionally logs a warning and counts
(octo.secrets.unreadable, reason=corrupt) a corrupt value - never the value.
DescribeSecret(RtSecretValue, SecretAccessContext)
Like ISecretAttributeProtector.GetReadState(RtSecretValue, SecretAccessContext), plus storage form, key id and "set at"
(RtSecretValue.SetAt) - the data behind GraphQL isSet / keyMissing /
setAt and the secrets overview. Never decrypts.
SecretReadInfo DescribeSecret(RtSecretValue value, SecretAccessContext context)
Parameters
value RtSecretValue
The stored value; null = not set
context SecretAccessContext
Where the value comes from (log and counter tags only)
Returns
SecretReadInfo
The description
RevealOrNull(RtSecretValue, SecretAccessContext)
Reveal helper for the server-side paths that need the plaintext (controller adapter
configuration, mesh adapter RevealSecret@1, identity providers, AI services, service-account
tokens; decisions 2026-10-06, item 2): like ISecretAttributeProtector.Unprotect(RtSecretValue, SecretAccessContext),
but a value that is stored and cannot be read is treated as NOT SET and returns null instead of
throwing - an unknown key id (UnknownSecretKeyIdException), a tampered / wrong-key
envelope (CryptographicException) and an enc:v2
envelope stored as a legacy string (SecretEnvelopeNotAllowedException). null,
empty values and legacy placeholders return null as well. The stored value is never changed.
string RevealOrNull(RtSecretValue value, SecretAccessContext context)
Parameters
value RtSecretValue
The stored value
context SecretAccessContext
Reader (decrypt counter, log)
Returns
String
The plaintext, or null when not set or unreadable
Remarks:
Configuration problems still throw: SecretEncryptionNotConfiguredException (no keys at
all / no legacy key on this host - the engine implementation; the default implementation maps an
unknown key id to null without that distinction) and, in strict mode, LegacyPlaintextSecretRejectedException.
The engine implementation logs an error (tenant, CK type, attribute, key id - never the value) and
counts octo.secrets.unreadable for every value it maps to null. The default
implementation only maps.
IsProtectedEnvelope(String)
Strict check: true only for a structurally valid enc:v1 or enc:v2 envelope
(see SecretEnvelope). A plaintext starting with enc: is not one.
bool IsProtectedEnvelope(string value)
Parameters
value String
Returns
TryParseEnvelope(String, out SecretEnvelopeInfo)
Parses an envelope strictly and returns version and key id.
bool TryParseEnvelope(string value, out SecretEnvelopeInfo info)
Parameters
value String
info SecretEnvelopeInfo
Returns
NeedsReprotect(RtSecretValue)
True when the value is not protected with the active key: pending, legacy (clear text or
enc:v1) or enc:v2 with another key id. The re-protect sweep (AB#5532) uses it.
bool NeedsReprotect(RtSecretValue value)
Parameters
value RtSecretValue
Returns
Reprotect(RtSecretValue, SecretAccessContext)
Returns the value protected with the active key; a value that already is, is returned
unchanged. RtSecretValue.SetAt is kept for a protected value, null for a
converted legacy value and the current time for a pending value. Decrypting for the re-encryption is counted like any other decrypt. Legacy clear
text is converted in strict mode as well (the strict check is bypassed and the read is counted
as a plaintext read): the encrypt / reprotect sweep must be able to clear the remainder. A legacy
value whose text is an enc:v2 envelope is refused like in
ISecretAttributeProtector.Unprotect(RtSecretValue, SecretAccessContext).
RtSecretValue Reprotect(RtSecretValue value, SecretAccessContext context)
Parameters
value RtSecretValue
context SecretAccessContext
Returns
Exceptions
SecretEnvelopeNotAllowedException
A legacy value whose text is an enc:v2 envelope