Files
esiur-dotnet/Libraries/Esiur/Security/Authority/Providers/Ppap/PpapRotationWire.cs
T
2026-07-16 19:36:30 +03:00

236 lines
8.7 KiB
C#

using System;
using System.IO;
namespace Esiur.Security.Authority.Providers.Ppap;
internal static partial class PpapWire
{
internal sealed class RotationOffer
{
public PpapIdentityRole Role;
public string Identity;
public long ExpectedVersion;
public byte[] Nonce;
public byte[] EncapsulationKey;
public PpapKdfProfile KdfProfile;
}
internal sealed class RotationChallenge
{
public PpapIdentityRole Role;
public byte[] Ciphertext;
}
internal sealed class RotationProof
{
public PpapIdentityRole Role;
public byte[] Proof;
}
internal static byte[] EncodeRotationStart(PpapIdentityRole role)
=> Encode(PpapMessageType.RotationStart,
payload => payload.WriteByte(ValidateRole(role)));
internal static PpapIdentityRole DecodeRotationStart(object data)
=> DecodeRoleOnly(data, PpapMessageType.RotationStart);
internal static byte[] EncodeRotationOffer(PpapIdentityRole role, string identity,
long expectedVersion, byte[] nonce, byte[] encapsulationKey,
PpapKdfProfile kdfProfile)
{
ValidateRole(role);
if (expectedVersion < 1 || expectedVersion == long.MaxValue)
throw new InvalidDataException("Invalid rotation version.");
ValidateExact(nonce, PpapProtocol.RegistrationNonceLength, nameof(nonce));
PpapCryptography.ValidatePublicKey(encapsulationKey);
if (kdfProfile == null)
throw new ArgumentNullException(nameof(kdfProfile));
var identityBytes = PpapCryptography.EncodeUtf8(
PpapCryptography.NormalizeIdentity(identity),
PpapProtocol.MaximumIdentityBytes, nameof(identity));
var descriptor = EncodeDescriptor(new PpapRegistrationDescriptor(
PpapIdentityKind.PasswordDerived, expectedVersion + 1, nonce, kdfProfile));
try
{
return Encode(PpapMessageType.RotationOffer, payload =>
{
payload.WriteByte((byte)role);
WriteField(payload, identityBytes);
WriteInt64(payload, expectedVersion);
WriteField(payload, encapsulationKey);
WriteField(payload, descriptor);
});
}
finally
{
PpapCryptography.Clear(identityBytes);
PpapCryptography.Clear(descriptor);
}
}
internal static RotationOffer DecodeRotationOffer(object data)
{
var payload = DecodeEnvelope(RequireWireBytes(data), PpapMessageType.RotationOffer);
var offset = 0;
var role = ReadRole(payload, ref offset);
var identityBytes = ReadField(payload, ref offset,
PpapProtocol.MaximumIdentityBytes);
var expectedVersion = ReadInt64(payload, ref offset);
var key = ReadField(payload, ref offset, PpapProtocol.PublicKeyLength);
var descriptorBytes = ReadField(payload, ref offset, 128);
RequireEnd(payload, offset);
if (expectedVersion < 1 || expectedVersion == long.MaxValue)
throw new InvalidDataException("Invalid rotation version.");
PpapCryptography.ValidatePublicKey(key);
var identity = PpapCryptography.NormalizeIdentity(
PpapCryptography.DecodeUtf8(identityBytes,
PpapProtocol.MaximumIdentityBytes, "identity"));
var descriptor = DecodeDescriptor(descriptorBytes);
if (descriptor.Kind != PpapIdentityKind.PasswordDerived
|| descriptor.Version != expectedVersion + 1)
throw new InvalidDataException("Invalid rotated registration descriptor.");
PpapCryptography.Clear(identityBytes);
PpapCryptography.Clear(descriptorBytes);
return new RotationOffer
{
Role = role,
Identity = identity,
ExpectedVersion = expectedVersion,
Nonce = descriptor.Nonce,
EncapsulationKey = key,
KdfProfile = descriptor.KdfProfile,
};
}
internal static byte[] EncodeRotationChallenge(PpapIdentityRole role,
byte[] ciphertext)
{
ValidateRole(role);
ValidateExact(ciphertext, PpapProtocol.CiphertextLength, nameof(ciphertext));
return Encode(PpapMessageType.RotationChallenge, payload =>
{
payload.WriteByte((byte)role);
WriteField(payload, ciphertext);
});
}
internal static RotationChallenge DecodeRotationChallenge(object data)
{
var payload = DecodeEnvelope(RequireWireBytes(data),
PpapMessageType.RotationChallenge);
var offset = 0;
var role = ReadRole(payload, ref offset);
var ciphertext = ReadField(payload, ref offset,
PpapProtocol.CiphertextLength);
RequireEnd(payload, offset);
ValidateExact(ciphertext, PpapProtocol.CiphertextLength,
nameof(ciphertext));
return new RotationChallenge { Role = role, Ciphertext = ciphertext };
}
internal static byte[] EncodeRotationProof(PpapIdentityRole role, byte[] proof)
{
ValidateRole(role);
ValidateExact(proof, PpapProtocol.HashLength, nameof(proof));
return Encode(PpapMessageType.RotationProof, payload =>
{
payload.WriteByte((byte)role);
WriteField(payload, proof);
});
}
internal static RotationProof DecodeRotationProof(object data)
{
var payload = DecodeEnvelope(RequireWireBytes(data), PpapMessageType.RotationProof);
var offset = 0;
var role = ReadRole(payload, ref offset);
var proof = ReadField(payload, ref offset, PpapProtocol.HashLength);
RequireEnd(payload, offset);
ValidateExact(proof, PpapProtocol.HashLength, nameof(proof));
return new RotationProof { Role = role, Proof = proof };
}
internal static byte[] EncodeRotationCommit(PpapIdentityRole role, long version)
=> EncodeRoleVersion(PpapMessageType.RotationCommit, role, version);
internal static long DecodeRotationCommit(object data,
PpapIdentityRole expectedRole)
=> DecodeRoleVersion(data, PpapMessageType.RotationCommit, expectedRole);
internal static byte[] EncodeRotationCommitAck(PpapIdentityRole role, long version)
=> EncodeRoleVersion(PpapMessageType.RotationCommitAck, role, version);
internal static long DecodeRotationCommitAck(object data,
PpapIdentityRole expectedRole)
=> DecodeRoleVersion(data, PpapMessageType.RotationCommitAck, expectedRole);
internal static byte[] EncodeRotationDone()
=> Encode(PpapMessageType.RotationDone, _ => { });
internal static void DecodeRotationDone(object data)
{
var payload = DecodeEnvelope(RequireWireBytes(data), PpapMessageType.RotationDone);
RequireEnd(payload, 0);
}
internal static PpapMessageType PeekMessageType(object data)
{
var input = RequireWireBytes(data);
if (input.Length < 10)
throw new InvalidDataException("Invalid PPAP message length.");
return (PpapMessageType)input[5];
}
static byte[] EncodeRoleVersion(PpapMessageType type, PpapIdentityRole role,
long version)
{
ValidateRole(role);
if (version < 2)
throw new InvalidDataException("Invalid committed registration version.");
return Encode(type, payload =>
{
payload.WriteByte((byte)role);
WriteInt64(payload, version);
});
}
static long DecodeRoleVersion(object data, PpapMessageType type,
PpapIdentityRole expectedRole)
{
var payload = DecodeEnvelope(RequireWireBytes(data), type);
var offset = 0;
var role = ReadRole(payload, ref offset);
var version = ReadInt64(payload, ref offset);
RequireEnd(payload, offset);
if (role != expectedRole || version < 2)
throw new InvalidDataException("Unexpected rotation commit.");
return version;
}
static PpapIdentityRole DecodeRoleOnly(object data, PpapMessageType type)
{
var payload = DecodeEnvelope(RequireWireBytes(data), type);
var offset = 0;
var role = ReadRole(payload, ref offset);
RequireEnd(payload, offset);
return role;
}
static PpapIdentityRole ReadRole(byte[] payload, ref int offset)
{
if (offset >= payload.Length)
throw new InvalidDataException("Missing PPAP identity role.");
var role = (PpapIdentityRole)payload[offset++];
ValidateRole(role);
return role;
}
static byte ValidateRole(PpapIdentityRole role)
{
if (role != PpapIdentityRole.Initiator
&& role != PpapIdentityRole.Responder)
throw new InvalidDataException("Invalid PPAP identity role.");
return (byte)role;
}
}