如何在 .NET 中复制蓝牙的 CCM 方案?

How do I replicate Bluetooth's CCM scheme in .NET?

我正在研究一种需要对固件映像进行端到端加密的固件更新方案。目标设备是蓝牙低功耗芯片,硬件支持蓝牙规范 AES-CCM 中指定的加密技术。我们想利用这个硬件来最小化代码大小和速度,所以我们需要以硬件构建的格式加密固件映像。

因此,我正在尝试使用 .NET 的 AesManaged class such that I can reproduce the data samples given in the Bluetooth Spec(第 1547 页),但我没有得到相同的输出。这是示例数据:

Payload byte length: 08
K: 89678967 89678967 45234523 45234523
Payload counter: 0000bc614e
Zero-length ACL-U Continuation: 0
Direction: 0
Initialization vector: 66778899 aabbccdd
LT_ADDR: 1
Packet Type: 3
LLID: 2
Payload: 68696a6b 6c6d6e6f

B0: 494e61bc 0000ddcc bbaa9988 77660008
B1: 00190200 00000000 00000000 00000000
B2: 68696a6b 6c6d6e6f 00000000 00000000

Y0: 95ddc3d4 2c9a70f1 61a28ee2 c08271ab
Y1: 418635ff 54615443 8aceca41 fe274779
Y2: 08d78b32 9d78ed33 b285fc42 e178d781

T: 08d78b32

CTR0: 014e61bc 0000ddcc bbaa9988 77660000
CTR1: 014e61bc 0000ddcc bbaa9988 77660001

S0: b90f2b23 f63717d3 38e0559d 1e7e785e
S1: d8c7e3e1 02050abb 025d0895 17cbe5fb

MIC: b1d8a011
Encrypted payload: b0ae898a 6e6864d4

现在,我很乐意在没有身份验证的情况下进行加密。我注意到 MIC 和加密有效载荷分别是 T 和有效载荷与 S0 和 S1 的异或,所以我的目标只是生成 S0。我的理解是,我应该能够通过使用键 K:

对 CTR0 数组进行 ECB 处理来做到这一点
//I've tried a few endian-ness permutations of K, none work
byte[] sampleKey = { 0x23, 0x45, 0x23, 0x45, 0x23, 0x45, 0x23, 0x45,
                    0x67, 0x89, 0x67, 0x89, 0x67, 0x89, 0x67, 0x89};
byte[] sampleCtr0 = { 01, 0x4e, 0x61, 0xbc, 00, 00, 0xdd, 0xcc,
                    0xbb, 0xaa, 0x99, 0x88, 0x77, 0x66, 00, 00 };
byte[] encrypted;

using (AesManaged aesAlg = new AesManaged())
{
    aesAlg.Mode = CipherMode.ECB; //CTR implemented as ECB w/ manually-incrementing counter

    // Create an encrytor to perform the stream transform.
    ICryptoTransform encryptor = aesAlg.CreateEncryptor(sampleKey, zeros); //zeros is a byte array of 16 0's

    // Create the streams used for encryption.
    using (MemoryStream msEncrypt = new MemoryStream())
    {
        using (CryptoStream csEncrypt = new CryptoStream(msEncrypt, encryptor, CryptoStreamMode.Write))
        {
            using (StreamWriter swEncrypt = new StreamWriter(csEncrypt))
            {
                //Write all data to the stream.
                swEncrypt.Write(sampleCtr0);
            }
            encrypted = msEncrypt.ToArray();
        }
    }
}

我希望在 encrypted 中看到 S0,但我没有。怎么了?

原来是使用 StreamWriter 的问题。删除它并用 csEncrypt.Write() 替换它后,我得到了预期的输出。

我仍然不太明白我的解决方法,所以我正要编辑这个问题,但鉴于这个问题可能与密码学无关,我认为最好将其作为一个单独的问题来解决。或者,如果有人可以解释修复,我会更改已接受的答案。

编辑:Dark Falcon 知道了。

流或其源流之一在复制之前可能需要 flush()。否则,如果未完成,它可以截断结尾。

这是我的 AES-CCM 实现,C# 2.0 兼容:

请注意,需要一些 here 可用的字节运算 类(例如 XOR):

/* Copyright (C) 2020 Tal Aloni <tal.aloni.il@gmail.com>. All rights reserved.
 * 
 * You can redistribute this program and/or modify it under the terms of
 * the GNU Lesser Public License as published by the Free Software Foundation,
 * either version 3 of the License, or (at your option) any later version.
 */
using System;
using System.IO;
using System.Security.Cryptography;

namespace Utilities
{
    /// <summary>
    /// Implements the Counter with CBC-MAC (CCM) detailed in RFC 3610
    /// </summary>
    public static class AesCcm
    {
        private static byte[] CalculateMac(byte[] key, byte[] nonce, byte[] data, byte[] associatedData, int signatureLength)
        {
            byte[] messageToAuthenticate = BuildB0Block(nonce, true, signatureLength, data.Length);
            if (associatedData.Length > 0)
            {
                if (associatedData.Length >= 65280)
                {
                    throw new NotSupportedException("Associated data length of 65280 or more is not supported");
                }

                byte[] associatedDataLength = BigEndianConverter.GetBytes((ushort)associatedData.Length);
                messageToAuthenticate = ByteUtils.Concatenate(messageToAuthenticate, associatedDataLength);
                messageToAuthenticate = ByteUtils.Concatenate(messageToAuthenticate, associatedData);
                int associatedDataPaddingLength = (16 - (messageToAuthenticate.Length % 16)) % 16;
                messageToAuthenticate = ByteUtils.Concatenate(messageToAuthenticate, new byte[associatedDataPaddingLength]);
            }

            messageToAuthenticate = ByteUtils.Concatenate(messageToAuthenticate, data);

            int dataPaddingLength = (16 - (messageToAuthenticate.Length % 16)) % 16;
            messageToAuthenticate = ByteUtils.Concatenate(messageToAuthenticate, new byte[dataPaddingLength]);

            byte[] encrypted = AesEncrypt(key, new byte[16], messageToAuthenticate, CipherMode.CBC);

            return ByteReader.ReadBytes(encrypted, messageToAuthenticate.Length - 16, signatureLength);
        }

        public static byte[] Encrypt(byte[] key, byte[] nonce, byte[] data, byte[] associatedData, int signatureLength, out byte[] signature)
        {
            if (nonce.Length < 7 || nonce.Length > 13)
            {
                throw new ArgumentException("nonce length must be between 7 and 13 bytes");
            }

            if (signatureLength < 4 || signatureLength > 16 || (signatureLength % 2 == 1))
            {
                throw new ArgumentException("signature length must be an even number between 4 and 16 bytes");
            }

            byte[] keyStream = BuildKeyStream(key, nonce, data.Length);

            byte[] mac = CalculateMac(key, nonce, data, associatedData, signatureLength);
            signature = ByteUtils.XOR(keyStream, 0, mac, 0, mac.Length);
            return ByteUtils.XOR(data, 0, keyStream, 16, data.Length);
        }

        public static byte[] DecryptAndAuthenticate(byte[] key, byte[] nonce, byte[] encryptedData, byte[] associatedData, byte[] signature)
        {
            if (nonce.Length < 7 || nonce.Length > 13)
            {
                throw new ArgumentException("nonce length must be between 7 and 13 bytes");
            }

            if (signature.Length < 4 || signature.Length > 16 || (signature.Length % 2 == 1))
            {
                throw new ArgumentException("signature length must be an even number between 4 and 16 bytes");
            }

            byte[] keyStream = BuildKeyStream(key, nonce, encryptedData.Length);

            byte[] data = ByteUtils.XOR(encryptedData, 0, keyStream, 16, encryptedData.Length);

            byte[] mac = CalculateMac(key, nonce, data, associatedData, signature.Length);
            byte[] expectedSignature = ByteUtils.XOR(keyStream, 0, mac, 0, mac.Length);
            if (!ByteUtils.AreByteArraysEqual(expectedSignature, signature))
            {
                throw new CryptographicException("The computed authentication value did not match the input");
            }
            return data;
        }

        private static byte[] BuildKeyStream(byte[] key, byte[] nonce, int dataLength)
        {
            int paddingLength = (16 - (dataLength % 16) % 16);
            int keyStreamLength = 16 + dataLength + paddingLength;
            int KeyStreamBlockCount = keyStreamLength / 16;
            byte[] keyStreamInput = new byte[keyStreamLength];
            for (int index = 0; index < KeyStreamBlockCount; index++)
            {
                byte[] aBlock = BuildABlock(nonce, index);
                ByteWriter.WriteBytes(keyStreamInput, index * 16, aBlock);
            }

            return AesEncrypt(key, new byte[16], keyStreamInput, CipherMode.ECB);
        }

        private static byte[] BuildB0Block(byte[] nonce, bool hasAssociatedData, int signatureLength, int messageLength)
        {
            byte[] b0 = new byte[16];
            Array.Copy(nonce, 0, b0, 1, nonce.Length);
            int lengthFieldLength = 15 - nonce.Length;
            b0[0] = ComputeFlagsByte(hasAssociatedData, signatureLength, lengthFieldLength);

            int temp = messageLength;
            for (int index = 15; index > 15 - lengthFieldLength; index--)
            {
                b0[index] = (byte)(temp % 256);
                temp /= 256;
            }

            return b0;
        }

        private static byte[] BuildABlock(byte[] nonce, int blockIndex)
        {
            byte[] aBlock = new byte[16];
            Array.Copy(nonce, 0, aBlock, 1, nonce.Length);
            int lengthFieldLength = 15 - nonce.Length;
            aBlock[0] = (byte)(lengthFieldLength - 1);

            int temp = blockIndex;
            for (int index = 15; index > 15 - lengthFieldLength; index--)
            {
                aBlock[index] = (byte)(temp % 256);
                temp /= 256;
            }

            return aBlock;
        }

        private static byte ComputeFlagsByte(bool hasAssociatedData, int signatureLength, int lengthFieldLength)
        {
            byte flags = 0;
            if (hasAssociatedData)
            {
                flags |= 0x40;
            }

            flags |= (byte)(lengthFieldLength - 1); // L'
            flags |= (byte)(((signatureLength - 2) / 2) << 3); // M'

            return flags;
        }

        private static byte[] AesEncrypt(byte[] key, byte[] iv, byte[] data, CipherMode cipherMode)
        {
            using (MemoryStream ms = new MemoryStream())
            {
                RijndaelManaged aes = new RijndaelManaged();
                aes.Mode = cipherMode;
                aes.Padding = PaddingMode.None;

                using (CryptoStream cs = new CryptoStream(ms, aes.CreateEncryptor(key, iv), CryptoStreamMode.Write))
                {
                    cs.Write(data, 0, data.Length);
                    cs.FlushFinalBlock();

                    return ms.ToArray();
                }
            }
        }
    }
}

没有方法StreamWriter.Write(byte[])。相反,您正在调用 StreamWriter.Write(object),它会在对象上调用 ToString。这将返回“System.Byte[]”,然后将其进行 UTF8 编码并写入您的 CryptoStream

byte[] sampleCtr0 = { 01, 0x4e, 0x61, 0xbc, 00, 00, 0xdd, 0xcc,
                    0xbb, 0xaa, 0x99, 0x88, 0x77, 0x66, 00, 00 };
using (var mem = new MemoryStream())
{
    using (var wri = new StreamWriter(mem))
    {
        //Write all data to the stream.
        wri.Write(sampleCtr0);
    }
    Console.WritELine(Encoding.UTF8.GetString(mem.ToArray()));
}

产生:

System.Byte[]

不要使用 StreamWriter 将二进制数据写入流。使用 Stream.Write 将数据直接写入流(就像您所做的那样)或使用 BinaryWriter.

StreamWriter 用于编写必须通过传递给构造函数的 Encoding 编码为字节的字符。默认为 Encoding.UTF8.