Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save badcommandorfilename/a6dee79ff2d46be29aeb5a9890cb8d90 to your computer and use it in GitHub Desktop.
Save badcommandorfilename/a6dee79ff2d46be29aeb5a9890cb8d90 to your computer and use it in GitHub Desktop.
Standalone (mostly) SP800_108_CTR_HMACSHA512 Key Derivation Function for ASPNetCore cookie sharing
///This is a Frankenstein class that can extract the AES key from a KDK as described in:
/// https://docs.microsoft.com/en-us/aspnet/core/security/data-protection/implementation/subkeyderivation?view=aspnetcore-2.2
///With a bit of luck, this should let an ASP.NET app decrypt cookies generated by an ASPNETCore app
///Still consult https://docs.microsoft.com/en-us/aspnet/core/security/cookie-sharing?view=aspnetcore-2.2 to share cookies
///Credit for most of the code is from various parts of https://github.com/aspnet/AspNetCore/tree/master/src/DataProtection/DataProtection/src
public unsafe class CookieDataProtector : IDataProtector
{
readonly string _base64MasterKey;
public string ApplicationName { get; set; }
public string[] Purposes { get; set; }
public CookieDataProtector(string base64MasterKey, string applicationName, params string[] purposes)
{
Purposes = purposes;
ApplicationName = applicationName;
_base64MasterKey = base64MasterKey;
}
public IDataProtector CreateProtector(string purpose)
{
return this;
}
public byte[] Protect(byte[] plaintext)
{
var serviceProtector = CreateProtector("");
return serviceProtector.Protect(plaintext);
}
public byte[] Unprotect(byte[] protectedData)
{
//https://stackoverflow.com/questions/42842511/how-to-manually-decrypt-an-asp-net-core-authentication-cookie/42857830
//var elements = keyChain.GetAllElements(); //If you have a handle to a KeyChain
var kdk = Convert.FromBase64String(_base64MasterKey);
// Parse the payload version number and key id.
uint magicHeaderFromPayload;
Guid keyIdFromPayload;
fixed (byte* pbInput = protectedData)
{
magicHeaderFromPayload = ReadBigEndian32BitInteger(pbInput);
keyIdFromPayload = Read32bitAlignedGuid(&pbInput[sizeof(uint)]);
}
///Important!
///The "Purpose" header is actually [Application Name] + [Purpose 1] ... [Purpose N]
///All these strings MUST MATCH the cookie generator EXACTLY
var _aadTemplate = new AdditionalAuthenticatedDataTemplate(
new [] { ApplicationName } //Application Name as per services.AddDataProtection().SetApplicationName("...")
.Union(Purposes) //Other purposes e.g. .CreateProtector(...
.ToArray());
ArraySegment<byte> ciphertext = new ArraySegment<byte>(protectedData, sizeof(uint) + sizeof(Guid), protectedData.Length - (sizeof(uint) + sizeof(Guid))); // chop off magic header + encryptor id
ArraySegment<byte> additionalAuthenticatedData = new ArraySegment<byte>(_aadTemplate.GetAadForKey(keyIdFromPayload, isProtecting: false));
///Refer to https://docs.microsoft.com/en-us/aspnet/core/security/data-protection/implementation/subkeyderivation?view=aspnetcore-2.2
var m = new ManagedAuthenticatedEncryptor(
kdk,
Aes.Create,
256 / 8, //32 byte AES key
() => new HMACSHA256()); //SHA 256 validation algorithm
// Perform the decryption operation.
var plainBytes = m.Decrypt(ciphertext, additionalAuthenticatedData);
//Get the decrypted cookie as plain text
UTF8Encoding specialUtf8Encoding = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true);
string plainText = specialUtf8Encoding.GetString(plainBytes); //Not used, but sanity check here
return plainBytes;
}
// Helper function to write a GUID to a 32-bit alignment; useful on ARM where unaligned reads
// can result in weird behaviors at runtime.
private static void Write32bitAlignedGuid(void* ptr, Guid value)
{
Debug.Assert((long)ptr % 4 == 0);
((int*)ptr)[0] = ((int*)&value)[0];
((int*)ptr)[1] = ((int*)&value)[1];
((int*)ptr)[2] = ((int*)&value)[2];
((int*)ptr)[3] = ((int*)&value)[3];
}
// Helper function to read a GUID from a 32-bit alignment; useful on architectures where unaligned reads
// can result in weird behaviors at runtime.
private static Guid Read32bitAlignedGuid(void* ptr)
{
Debug.Assert((long)ptr % 4 == 0);
Guid retVal;
((int*)&retVal)[0] = ((int*)ptr)[0];
((int*)&retVal)[1] = ((int*)ptr)[1];
((int*)&retVal)[2] = ((int*)ptr)[2];
((int*)&retVal)[3] = ((int*)ptr)[3];
return retVal;
}
private static uint ReadBigEndian32BitInteger(byte* ptr)
{
return ((uint)ptr[0] << 24)
| ((uint)ptr[1] << 16)
| ((uint)ptr[2] << 8)
| ((uint)ptr[3]);
}
private struct AdditionalAuthenticatedDataTemplate
{
//ref https://docs.microsoft.com/en-us/aspnet/core/security/data-protection/implementation/subkeyderivation?view=aspnetcore-2.2
private const uint MAGIC_HEADER_V0 = 0x09F0C9F0;
private byte[] _aadTemplate;
public AdditionalAuthenticatedDataTemplate(params string[] purposes)
{
const int MEMORYSTREAM_DEFAULT_CAPACITY = 0x100; // matches MemoryStream.EnsureCapacity
var ms = new MemoryStream(MEMORYSTREAM_DEFAULT_CAPACITY);
// additionalAuthenticatedData := { magicHeader (32-bit) || keyId || purposeCount (32-bit) || (purpose)* }
// purpose := { utf8ByteCount (7-bit encoded) || utf8Text }
using (var writer = new PurposeBinaryWriter(ms))
{
writer.WriteBigEndian(MAGIC_HEADER_V0);
Debug.Assert(ms.Position == sizeof(uint));
var posPurposeCount = writer.Seek(sizeof(Guid), SeekOrigin.Current); // skip over where the key id will be stored; we'll fill it in later
writer.Seek(sizeof(uint), SeekOrigin.Current); // skip over where the purposeCount will be stored; we'll fill it in later
uint purposeCount = 0;
foreach (string purpose in purposes)
{
Debug.Assert(purpose != null);
writer.Write(purpose); // prepends length as a 7-bit encoded integer
purposeCount++;
}
// Once we have written all the purposes, go back and fill in 'purposeCount'
writer.Seek(checked((int)posPurposeCount), SeekOrigin.Begin);
writer.WriteBigEndian(purposeCount);
}
_aadTemplate = ms.ToArray();
}
public byte[] GetAadForKey(Guid keyId, bool isProtecting)
{
// Multiple threads might be trying to read and write the _aadTemplate field
// simultaneously. We need to make sure all accesses to it are thread-safe.
var existingTemplate = Volatile.Read(ref _aadTemplate);
Debug.Assert(existingTemplate.Length >= sizeof(uint) /* MAGIC_HEADER */ + sizeof(Guid) /* keyId */);
// If the template is already initialized to this key id, return it.
// The caller will not mutate it.
fixed (byte* pExistingTemplate = existingTemplate)
{
if (Read32bitAlignedGuid(&pExistingTemplate[sizeof(uint)]) == keyId)
{
return existingTemplate;
}
}
// Clone since we're about to make modifications.
// If this is an encryption operation, we only ever encrypt to the default key,
// so we should replace the existing template. This could occur after the protector
// has already been created, such as when the underlying key ring has been modified.
byte[] newTemplate = (byte[])existingTemplate.Clone();
fixed (byte* pNewTemplate = newTemplate)
{
Write32bitAlignedGuid(&pNewTemplate[sizeof(uint)], keyId);
if (isProtecting)
{
Volatile.Write(ref _aadTemplate, newTemplate);
}
return newTemplate;
}
}
private sealed class PurposeBinaryWriter : BinaryWriter
{
public static readonly UTF8Encoding SecureUtf8Encoding = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true);
public PurposeBinaryWriter(MemoryStream stream) : base(stream, SecureUtf8Encoding, leaveOpen: true) { }
// Writes a big-endian 32-bit integer to the underlying stream.
public void WriteBigEndian(uint value)
{
var outStream = BaseStream; // property accessor also performs a flush
outStream.WriteByte((byte)(value >> 24));
outStream.WriteByte((byte)(value >> 16));
outStream.WriteByte((byte)(value >> 8));
outStream.WriteByte((byte)(value));
}
}
}
// An encryptor which does Encrypt(CBC) + HMAC using SymmetricAlgorithm and HashAlgorithm.
// The payloads produced by this encryptor should be compatible with the payloads
// produced by the CNG-based Encrypt(CBC) + HMAC authenticated encryptor.
internal unsafe sealed class ManagedAuthenticatedEncryptor
{
// Even when IVs are chosen randomly, CBC is susceptible to IV collisions within a single
// key. For a 64-bit block cipher (like 3DES), we'd expect a collision after 2^32 block
// encryption operations, which a high-traffic web server might perform in mere hours.
// AES and other 128-bit block ciphers are less susceptible to this due to the larger IV
// space, but unfortunately some organizations require older 64-bit block ciphers. To address
// the collision issue, we'll feed 128 bits of entropy to the KDF when performing subkey
// generation. This creates >= 192 bits total entropy for each operation, so we shouldn't
// expect a collision until >= 2^96 operations. Even 2^80 operations still maintains a <= 2^-32
// probability of collision, and this is acceptable for the expected KDK lifetime.
private const int KEY_MODIFIER_SIZE_IN_BYTES = 128 / 8;
private static readonly Func<byte[], HashAlgorithm> _kdkPrfFactory = key => new HMACSHA512(key); // currently hardcoded to SHA512
private readonly byte[] _contextHeader;
private readonly byte[] _keyDerivationKey;
private readonly Func<SymmetricAlgorithm> _symmetricAlgorithmFactory;
private readonly int _symmetricAlgorithmBlockSizeInBytes;
private readonly int _symmetricAlgorithmSubkeyLengthInBytes;
private readonly int _validationAlgorithmDigestLengthInBytes;
private readonly int _validationAlgorithmSubkeyLengthInBytes;
private readonly Func<KeyedHashAlgorithm> _validationAlgorithmFactory;
public ManagedAuthenticatedEncryptor(byte[] keyDerivationKey, Func<SymmetricAlgorithm> symmetricAlgorithmFactory, int symmetricAlgorithmKeySizeInBytes, Func<KeyedHashAlgorithm> validationAlgorithmFactory)
{
//_genRandom = genRandom ?? ManagedGenRandomImpl.Instance;
_keyDerivationKey = keyDerivationKey;
// Validate that the symmetric algorithm has the properties we require
using (var symmetricAlgorithm = symmetricAlgorithmFactory())
{
_symmetricAlgorithmFactory = symmetricAlgorithmFactory;
_symmetricAlgorithmBlockSizeInBytes = symmetricAlgorithm.BlockSize / 8;
_symmetricAlgorithmSubkeyLengthInBytes = symmetricAlgorithmKeySizeInBytes;
}
// Validate that the MAC algorithm has the properties we require
using (var validationAlgorithm = validationAlgorithmFactory())
{
_validationAlgorithmFactory = validationAlgorithmFactory;
_validationAlgorithmDigestLengthInBytes = validationAlgorithm.HashSize / 8;
_validationAlgorithmSubkeyLengthInBytes = _validationAlgorithmDigestLengthInBytes; // for simplicity we'll generate MAC subkeys with a length equal to the digest length
}
_contextHeader = CreateContextHeader();
}
private byte[] CreateContextHeader()
{
var EMPTY_ARRAY = new byte[0];
var EMPTY_ARRAY_SEGMENT = new ArraySegment<byte>(EMPTY_ARRAY);
var retVal = new byte[checked(
1 /* KDF alg */
+ 1 /* chaining mode */
+ sizeof(uint) /* sym alg key size */
+ sizeof(uint) /* sym alg block size */
+ sizeof(uint) /* hmac alg key size */
+ sizeof(uint) /* hmac alg digest size */
+ _symmetricAlgorithmBlockSizeInBytes /* ciphertext of encrypted empty string */
+ _validationAlgorithmDigestLengthInBytes /* digest of HMACed empty string */)];
var idx = 0;
// First is the two-byte header
retVal[idx++] = 0; // 0x00 = SP800-108 CTR KDF w/ HMACSHA512 PRF
retVal[idx++] = 0; // 0x00 = CBC encryption + HMAC authentication
// Next is information about the symmetric algorithm (key size followed by block size)
BitHelpers.WriteTo(retVal, ref idx, _symmetricAlgorithmSubkeyLengthInBytes);
BitHelpers.WriteTo(retVal, ref idx, _symmetricAlgorithmBlockSizeInBytes);
// Next is information about the keyed hash algorithm (key size followed by digest size)
BitHelpers.WriteTo(retVal, ref idx, _validationAlgorithmSubkeyLengthInBytes);
BitHelpers.WriteTo(retVal, ref idx, _validationAlgorithmDigestLengthInBytes);
// See the design document for an explanation of the following code.
var tempKeys = new byte[_symmetricAlgorithmSubkeyLengthInBytes + _validationAlgorithmSubkeyLengthInBytes];
ManagedSP800_108_CTR_HMACSHA512.DeriveKeys(
kdk: EMPTY_ARRAY,
label: EMPTY_ARRAY_SEGMENT,
context: EMPTY_ARRAY_SEGMENT,
prfFactory: _kdkPrfFactory,
output: new ArraySegment<byte>(tempKeys));
// At this point, tempKeys := { K_E || K_H }.
// Encrypt a zero-length input string with an all-zero IV and copy the ciphertext to the return buffer.
using (var symmetricAlg = CreateSymmetricAlgorithm())
{
using (var cryptoTransform = symmetricAlg.CreateEncryptor(
rgbKey: new ArraySegment<byte>(tempKeys, 0, _symmetricAlgorithmSubkeyLengthInBytes).ToArray(),
rgbIV: new byte[_symmetricAlgorithmBlockSizeInBytes]))
{
var ciphertext = cryptoTransform.TransformFinalBlock(EMPTY_ARRAY, 0, 0);
//CryptoUtil.Assert(ciphertext != null && ciphertext.Length == _symmetricAlgorithmBlockSizeInBytes, "ciphertext != null && ciphertext.Length == _symmetricAlgorithmBlockSizeInBytes");
Buffer.BlockCopy(ciphertext, 0, retVal, idx, ciphertext.Length);
}
}
idx += _symmetricAlgorithmBlockSizeInBytes;
// MAC a zero-length input string and copy the digest to the return buffer.
using (var hashAlg = CreateValidationAlgorithm(new ArraySegment<byte>(tempKeys, _symmetricAlgorithmSubkeyLengthInBytes, _validationAlgorithmSubkeyLengthInBytes).ToArray()))
{
var digest = hashAlg.ComputeHash(EMPTY_ARRAY);
//CryptoUtil.Assert(digest != null && digest.Length == _validationAlgorithmDigestLengthInBytes, "digest != null && digest.Length == _validationAlgorithmDigestLengthInBytes");
Buffer.BlockCopy(digest, 0, retVal, idx, digest.Length);
}
idx += _validationAlgorithmDigestLengthInBytes;
//CryptoUtil.Assert(idx == retVal.Length, "idx == retVal.Length");
// retVal := { version || chainingMode || symAlgKeySize || symAlgBlockSize || macAlgKeySize || macAlgDigestSize || E("") || MAC("") }.
return retVal;
}
private SymmetricAlgorithm CreateSymmetricAlgorithm()
{
var retVal = _symmetricAlgorithmFactory();
retVal.Mode = CipherMode.CBC; //Important! This is the correct CipherMode for cookie sharing
retVal.Padding = PaddingMode.PKCS7; //Important! This is the correct PaddingMode for cookie sharing
//NB: If you get "invalid padding" errors, it means that your decryption key was wrong. Changing the padding will just output garbage anyway.
return retVal;
}
private KeyedHashAlgorithm CreateValidationAlgorithm(byte[] key)
{
var retVal = _validationAlgorithmFactory();
retVal.Key = key;
return retVal;
}
public byte[] Decrypt(ArraySegment<byte> protectedPayload, ArraySegment<byte> additionalAuthenticatedData)
{
// Argument checking - input must at the absolute minimum contain a key modifier, IV, and MAC
if (protectedPayload.Count < checked(KEY_MODIFIER_SIZE_IN_BYTES + _symmetricAlgorithmBlockSizeInBytes + _validationAlgorithmDigestLengthInBytes))
{
throw new Exception();
}
// Assumption: protectedPayload := { keyModifier | IV | encryptedData | MAC(IV | encryptedPayload) }
try
{
// Step 1: Extract the key modifier and IV from the payload.
int keyModifierOffset; // position in protectedPayload.Array where key modifier begins
int ivOffset; // position in protectedPayload.Array where key modifier ends / IV begins
int ciphertextOffset; // position in protectedPayload.Array where IV ends / ciphertext begins
int macOffset; // position in protectedPayload.Array where ciphertext ends / MAC begins
int eofOffset; // position in protectedPayload.Array where MAC ends
checked
{
keyModifierOffset = protectedPayload.Offset;
ivOffset = keyModifierOffset + KEY_MODIFIER_SIZE_IN_BYTES;
ciphertextOffset = ivOffset + _symmetricAlgorithmBlockSizeInBytes;
}
ArraySegment<byte> keyModifier = new ArraySegment<byte>(protectedPayload.Array, keyModifierOffset, ivOffset - keyModifierOffset);
var iv = new byte[_symmetricAlgorithmBlockSizeInBytes];
Buffer.BlockCopy(protectedPayload.Array, ivOffset, iv, 0, iv.Length);
// Step 2: Decrypt the KDK and use it to restore the original encryption and MAC keys.
// We pin all unencrypted keys to limit their exposure via GC relocation.
var decryptedKdk = new byte[_keyDerivationKey.Length];
var decryptionSubkey = new byte[_symmetricAlgorithmSubkeyLengthInBytes];
var validationSubkey = new byte[_validationAlgorithmSubkeyLengthInBytes];
var derivedKeysBuffer = new byte[checked(decryptionSubkey.Length + validationSubkey.Length)];
fixed (byte* __unused__1 = decryptedKdk)
fixed (byte* __unused__2 = decryptionSubkey)
fixed (byte* __unused__3 = validationSubkey)
fixed (byte* __unused__4 = derivedKeysBuffer)
{
decryptedKdk = _keyDerivationKey;
ManagedSP800_108_CTR_HMACSHA512.DeriveKeysWithContextHeader(
kdk: decryptedKdk,
label: additionalAuthenticatedData,
contextHeader: _contextHeader,
context: keyModifier,
prfFactory: _kdkPrfFactory,
output: new ArraySegment<byte>(derivedKeysBuffer));
//This is the 32byte AES Key derived from the KDK
Buffer.BlockCopy(derivedKeysBuffer, 0, decryptionSubkey, 0, decryptionSubkey.Length);
//This is the 32byte SHA256 Key derived from the KDK
Buffer.BlockCopy(derivedKeysBuffer, decryptionSubkey.Length, validationSubkey, 0, validationSubkey.Length);
// Step 3: Calculate the correct MAC for this payload.
// correctHash := MAC(IV || ciphertext)
byte[] correctHash;
using (var hashAlgorithm = CreateValidationAlgorithm(validationSubkey))
{
checked
{
eofOffset = protectedPayload.Offset + protectedPayload.Count;
macOffset = eofOffset - _validationAlgorithmDigestLengthInBytes;
}
correctHash = hashAlgorithm.ComputeHash(protectedPayload.Array, ivOffset, macOffset - ivOffset);
}
// Step 4: Validate the MAC provided as part of the payload.
int i = 0;
foreach (var b in correctHash)
{
var mb = protectedPayload.Array[macOffset + i++];
Debug.Assert(b == mb);
}
// Step 5: Decipher the ciphertext and return it to the caller.
using (var symmetricAlgorithm = CreateSymmetricAlgorithm())
using (var cryptoTransform = symmetricAlgorithm.CreateDecryptor(decryptionSubkey, iv))
{
var outputStream = new MemoryStream();
using (var cryptoStream = new CryptoStream(outputStream, cryptoTransform, CryptoStreamMode.Write))
{
cryptoStream.Write(protectedPayload.Array, ciphertextOffset, macOffset - ciphertextOffset);
cryptoStream.FlushFinalBlock();
// At this point, outputStream := { plaintext }, and we're done!
return outputStream.ToArray();
}
}
}
}
catch (Exception ex)
{
throw ex;
}
}
internal static class ManagedSP800_108_CTR_HMACSHA512
{
public static void DeriveKeys(byte[] kdk, ArraySegment<byte> label, ArraySegment<byte> context, Func<byte[], HashAlgorithm> prfFactory, ArraySegment<byte> output)
{
// make copies so we can mutate these local vars
var outputOffset = output.Offset;
var outputCount = output.Count;
using (var prf = prfFactory(kdk))
{
// See SP800-108, Sec. 5.1 for the format of the input to the PRF routine.
var prfInput = new byte[checked(sizeof(uint) /* [i]_2 */ + label.Count + 1 /* 0x00 */ + context.Count + sizeof(uint) /* [K]_2 */)];
// Copy [L]_2 to prfInput since it's stable over all iterations
uint outputSizeInBits = (uint)checked((int)outputCount * 8);
prfInput[prfInput.Length - 4] = (byte)(outputSizeInBits >> 24);
prfInput[prfInput.Length - 3] = (byte)(outputSizeInBits >> 16);
prfInput[prfInput.Length - 2] = (byte)(outputSizeInBits >> 8);
prfInput[prfInput.Length - 1] = (byte)(outputSizeInBits);
// Copy label and context to prfInput since they're stable over all iterations
Buffer.BlockCopy(label.Array, label.Offset, prfInput, sizeof(uint), label.Count);
Buffer.BlockCopy(context.Array, context.Offset, prfInput, sizeof(int) + label.Count + 1, context.Count);
var prfOutputSizeInBytes = prf.HashSize / 8;//.GetDigestSizeInBytes();
for (uint i = 1; outputCount > 0; i++)
{
// Copy [i]_2 to prfInput since it mutates with each iteration
prfInput[0] = (byte)(i >> 24);
prfInput[1] = (byte)(i >> 16);
prfInput[2] = (byte)(i >> 8);
prfInput[3] = (byte)(i);
// Run the PRF and copy the results to the output buffer
var prfOutput = prf.ComputeHash(prfInput);
Debug.Assert(prfOutputSizeInBytes == prfOutput.Length, "prfOutputSizeInBytes == prfOutput.Length");
var numBytesToCopyThisIteration = Math.Min(prfOutputSizeInBytes, outputCount);
Buffer.BlockCopy(prfOutput, 0, output.Array, outputOffset, numBytesToCopyThisIteration);
Array.Clear(prfOutput, 0, prfOutput.Length); // contains key material, so delete it
// adjust offsets
outputOffset += numBytesToCopyThisIteration;
outputCount -= numBytesToCopyThisIteration;
}
}
}
public static void DeriveKeysWithContextHeader(byte[] kdk, ArraySegment<byte> label, byte[] contextHeader, ArraySegment<byte> context, Func<byte[], HashAlgorithm> prfFactory, ArraySegment<byte> output)
{
var combinedContext = new byte[checked(contextHeader.Length + context.Count)];
Buffer.BlockCopy(contextHeader, 0, combinedContext, 0, contextHeader.Length);
Buffer.BlockCopy(context.Array, context.Offset, combinedContext, contextHeader.Length, context.Count);
DeriveKeys(kdk, label, new ArraySegment<byte>(combinedContext), prfFactory, output);
}
}
internal static class BitHelpers
{
/// <summary>
/// Writes an unsigned 32-bit value to a memory address, big-endian.
/// </summary>
public static void WriteTo(void* ptr, uint value)
{
byte* bytePtr = (byte*)ptr;
bytePtr[0] = (byte)(value >> 24);
bytePtr[1] = (byte)(value >> 16);
bytePtr[2] = (byte)(value >> 8);
bytePtr[3] = (byte)(value);
}
public static void WriteTo(ref byte* ptr, uint value)
{
byte* pTemp = ptr;
pTemp[0] = (byte)(value >> 24);
pTemp[1] = (byte)(value >> 16);
pTemp[2] = (byte)(value >> 8);
pTemp[3] = (byte)(value);
ptr = &pTemp[4];
}
/// <summary>
/// Writes a signed 32-bit value to a memory address, big-endian.
/// </summary>
public static void WriteTo(byte[] buffer, ref int idx, int value)
{
WriteTo(buffer, ref idx, (uint)value);
}
/// <summary>
/// Writes a signed 32-bit value to a memory address, big-endian.
/// </summary>
public static void WriteTo(byte[] buffer, ref int idx, uint value)
{
buffer[idx++] = (byte)(value >> 24);
buffer[idx++] = (byte)(value >> 16);
buffer[idx++] = (byte)(value >> 8);
buffer[idx++] = (byte)(value);
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment