refactor: rename ScadaLink → ZB.MOM.WW.ScadaBridge (code + projects + namespaces)
Solution + 23 src projects + 26 test projects renamed; folders, csproj, namespaces, and ScadaLinkDbContext/ScadaBridgeDbContext class updated. ActorSystem "scadalink" → "scadabridge", Akka seed-node URLs migrated. SQL roles/logins, LDAP domains, CLI command name, and CLI config dir (~/.scadalink → ~/.scadabridge) also renamed. Build green; 5 Host.Tests fail awaiting SQL login rename in next commit. Pre-existing StaleTagMonitor timing flakes unchanged. Rename script committed at tools/rename-to-scadabridge.sh.
This commit is contained in:
@@ -0,0 +1,56 @@
|
||||
using System.Security.Cryptography;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Types.Transport;
|
||||
|
||||
namespace ZB.MOM.WW.ScadaBridge.Transport.Encryption;
|
||||
|
||||
/// <summary>
|
||||
/// T-005: computes the AES-GCM Associated Authenticated Data (AAD) for a bundle's
|
||||
/// encrypted payload. AAD is the SHA-256 of the manifest after normalising the two
|
||||
/// derivative fields (<c>ContentHash</c>, <c>Encryption</c>) to known sentinel
|
||||
/// values — those depend on the ciphertext and the IV, so they cannot themselves
|
||||
/// be authenticated, but every OTHER manifest field (<c>SourceEnvironment</c>,
|
||||
/// <c>ExportedBy</c>, <c>ScadaBridgeVersion</c>, <c>Summary</c>, <c>Contents</c>,
|
||||
/// <c>CreatedAtUtc</c>, …) participates in the GCM tag.
|
||||
/// <para>
|
||||
/// Threading this byte array through <c>AesGcm.Encrypt</c> / <c>AesGcm.Decrypt</c>
|
||||
/// makes the Step-4 "type the source environment name to confirm" gate
|
||||
/// tamper-evident: a flipped <c>SourceEnvironment</c> on a stolen bundle yields
|
||||
/// an <c>AuthenticationTagMismatchException</c> on decrypt instead of producing
|
||||
/// a valid plaintext with a forged origin label.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public static class BundleManifestAad
|
||||
{
|
||||
/// <summary>
|
||||
/// JSON options matching <c>BundleSerializer.JsonOptions</c> so the AAD bytes
|
||||
/// are stable across the encrypt + decrypt side.
|
||||
/// </summary>
|
||||
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||
{
|
||||
WriteIndented = false,
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
|
||||
Converters = { new JsonStringEnumConverter() },
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Computes the AAD bytes for the supplied manifest. The two derivative
|
||||
/// fields are normalised to fixed sentinels so the AAD is independent of the
|
||||
/// ciphertext / IV that will eventually populate them in the on-disk
|
||||
/// manifest.
|
||||
/// </summary>
|
||||
/// <param name="manifest">The manifest whose non-derivative fields should be authenticated.</param>
|
||||
/// <returns>SHA-256 digest of the canonicalised manifest bytes.</returns>
|
||||
public static byte[] Compute(BundleManifest manifest)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(manifest);
|
||||
var canonical = manifest with
|
||||
{
|
||||
ContentHash = string.Empty,
|
||||
Encryption = null,
|
||||
};
|
||||
var canonicalBytes = JsonSerializer.SerializeToUtf8Bytes(canonical, JsonOptions);
|
||||
return SHA256.HashData(canonicalBytes);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
using System.Security.Cryptography;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Types.Transport;
|
||||
|
||||
namespace ZB.MOM.WW.ScadaBridge.Transport.Encryption;
|
||||
|
||||
/// <summary>
|
||||
/// AES-256-GCM authenticated encryption with a PBKDF2-SHA256 derived key.
|
||||
/// Output format is <c>ciphertext || tag</c> (tag is the GCM authentication tag).
|
||||
/// Each encrypt call produces a fresh random salt + nonce so re-encrypting the
|
||||
/// same plaintext yields a different ciphertext.
|
||||
/// </summary>
|
||||
public sealed class BundleSecretEncryptor
|
||||
{
|
||||
private const int KeyBytes = 32; // AES-256.
|
||||
private const int SaltBytes = 16;
|
||||
private const int NonceBytes = 12; // GCM standard.
|
||||
private const int TagBytes = 16;
|
||||
|
||||
/// <summary>
|
||||
/// Encrypts plaintext using AES-256-GCM with a PBKDF2-derived key and a fresh random salt and nonce.
|
||||
/// </summary>
|
||||
/// <param name="plaintext">The data to encrypt.</param>
|
||||
/// <param name="passphrase">The passphrase used to derive the encryption key.</param>
|
||||
/// <param name="iterations">PBKDF2 iteration count for key derivation.</param>
|
||||
/// <param name="associatedData">
|
||||
/// T-005: optional AES-GCM Associated Authenticated Data — typically
|
||||
/// <see cref="BundleManifestAad.Compute"/>(manifest). Binds the manifest's
|
||||
/// non-derivative fields (source environment, exporter, summary, …) to the
|
||||
/// ciphertext so any tampering of those fields yields an authentication-tag
|
||||
/// mismatch on decrypt. Pass <see cref="ReadOnlySpan{Byte}.Empty"/> for the
|
||||
/// legacy no-AAD format; the decrypter must mirror the choice.
|
||||
/// </param>
|
||||
/// <returns>The ciphertext (with appended GCM tag) and the encryption metadata needed to decrypt.</returns>
|
||||
public (byte[] Ciphertext, EncryptionMetadata Metadata) Encrypt(
|
||||
ReadOnlySpan<byte> plaintext,
|
||||
string passphrase,
|
||||
int iterations,
|
||||
ReadOnlySpan<byte> associatedData = default)
|
||||
{
|
||||
var salt = RandomNumberGenerator.GetBytes(SaltBytes);
|
||||
var nonce = RandomNumberGenerator.GetBytes(NonceBytes);
|
||||
var key = DeriveKey(passphrase, salt, iterations);
|
||||
|
||||
var ciphertext = new byte[plaintext.Length];
|
||||
var tag = new byte[TagBytes];
|
||||
using var aes = new AesGcm(key, TagBytes);
|
||||
aes.Encrypt(nonce, plaintext, ciphertext, tag, associatedData);
|
||||
|
||||
// Format: ciphertext || tag.
|
||||
var output = new byte[ciphertext.Length + TagBytes];
|
||||
Buffer.BlockCopy(ciphertext, 0, output, 0, ciphertext.Length);
|
||||
Buffer.BlockCopy(tag, 0, output, ciphertext.Length, TagBytes);
|
||||
|
||||
return (output, new EncryptionMetadata(
|
||||
"AES-256-GCM", "PBKDF2-SHA256", iterations,
|
||||
Convert.ToBase64String(salt),
|
||||
Convert.ToBase64String(nonce)));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Decrypts a payload produced by <see cref="Encrypt"/> using the supplied metadata and passphrase.
|
||||
/// </summary>
|
||||
/// <param name="payload">The ciphertext with appended GCM tag.</param>
|
||||
/// <param name="metadata">Encryption metadata carrying the algorithm, KDF, salt, nonce, and iterations.</param>
|
||||
/// <param name="passphrase">The passphrase used to derive the decryption key.</param>
|
||||
/// <param name="associatedData">
|
||||
/// T-005: AAD that was passed to <see cref="Encrypt"/>. Must match exactly —
|
||||
/// any tampering of the manifest used to derive AAD yields an authentication-
|
||||
/// tag mismatch (surfaces as <see cref="CryptographicException"/>, specifically
|
||||
/// <c>AuthenticationTagMismatchException</c> on .NET 10).
|
||||
/// </param>
|
||||
/// <returns>The decrypted plaintext bytes.</returns>
|
||||
public byte[] Decrypt(
|
||||
ReadOnlySpan<byte> payload,
|
||||
EncryptionMetadata metadata,
|
||||
string passphrase,
|
||||
ReadOnlySpan<byte> associatedData = default)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(metadata);
|
||||
|
||||
if (metadata.Algorithm != "AES-256-GCM" || metadata.Kdf != "PBKDF2-SHA256")
|
||||
{
|
||||
throw new CryptographicException("Unsupported bundle encryption parameters.");
|
||||
}
|
||||
|
||||
var salt = Convert.FromBase64String(metadata.SaltB64);
|
||||
var nonce = Convert.FromBase64String(metadata.IvB64);
|
||||
var key = DeriveKey(passphrase, salt, metadata.Iterations);
|
||||
|
||||
if (payload.Length < TagBytes)
|
||||
{
|
||||
throw new CryptographicException("Bundle payload too short.");
|
||||
}
|
||||
|
||||
var ctLen = payload.Length - TagBytes;
|
||||
var ciphertext = payload[..ctLen];
|
||||
var tag = payload[ctLen..];
|
||||
|
||||
var plaintext = new byte[ctLen];
|
||||
using var aes = new AesGcm(key, TagBytes);
|
||||
aes.Decrypt(nonce, ciphertext, tag, plaintext, associatedData);
|
||||
return plaintext;
|
||||
}
|
||||
|
||||
private static byte[] DeriveKey(string passphrase, byte[] salt, int iterations)
|
||||
{
|
||||
return Rfc2898DeriveBytes.Pbkdf2(passphrase, salt, iterations, HashAlgorithmName.SHA256, KeyBytes);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user