From 23cc90c00be0e6f8c3eef0adfe1a0906fa77b346 Mon Sep 17 00:00:00 2001 From: Jason Graffius Date: Fri, 6 Dec 2024 20:39:01 +0000 Subject: [PATCH] pw_crypto: Add AES facade MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add an initial facade for AES that includes raw::EncryptBlock. No backends are implemented yet, so it is configured as a header library rather than a facade, but this will be updated when the first backend is implemented. Change-Id: Id152a22c52f44e7abc8fa9298e5f4bbeb7203fc3 Reviewed-on: https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/231911 Presubmit-Verified: CQ Bot Account Commit-Queue: Jason Graffius Lint: Lint 🤖 Reviewed-by: Ali Zhang --- docs/BUILD.gn | 1 + pw_crypto/BUILD.bazel | 26 +++ pw_crypto/BUILD.gn | 22 +++ pw_crypto/aes_test.cc | 127 +++++++++++++++ pw_crypto/docs.rst | 21 +++ pw_crypto/public/pw_crypto/aes.h | 148 ++++++++++++++++++ pw_crypto/public/pw_crypto/aes_backend.h | 27 ++++ pw_crypto/public/pw_crypto/aes_backend_defs.h | 79 ++++++++++ 8 files changed, 451 insertions(+) create mode 100644 pw_crypto/aes_test.cc create mode 100644 pw_crypto/public/pw_crypto/aes.h create mode 100644 pw_crypto/public/pw_crypto/aes_backend.h create mode 100644 pw_crypto/public/pw_crypto/aes_backend_defs.h diff --git a/docs/BUILD.gn b/docs/BUILD.gn index f33cc78b16..eaed6c2051 100644 --- a/docs/BUILD.gn +++ b/docs/BUILD.gn @@ -268,6 +268,7 @@ _doxygen_input_files = [ # keep-sorted: start "$dir_pw_containers/public/pw_containers/intrusive_multimap.h", "$dir_pw_containers/public/pw_containers/intrusive_multiset.h", "$dir_pw_containers/public/pw_containers/intrusive_set.h", + "$dir_pw_crypto/public/pw_crypto/aes.h", "$dir_pw_crypto/public/pw_crypto/ecdsa.h", "$dir_pw_crypto/public/pw_crypto/sha256.h", "$dir_pw_digital_io/public/pw_digital_io/digital_io.h", diff --git a/pw_crypto/BUILD.bazel b/pw_crypto/BUILD.bazel index 6dbbed2c34..d123d1ad4a 100644 --- a/pw_crypto/BUILD.bazel +++ b/pw_crypto/BUILD.bazel @@ -181,6 +181,32 @@ pw_cc_test( ], ) +cc_library( + name = "aes", + hdrs = [ + "public/pw_crypto/aes.h", + "public/pw_crypto/aes_backend.h", + "public/pw_crypto/aes_backend_defs.h", + ], + includes = ["public"], + deps = [ + "//pw_bytes", + "//pw_status", + ], +) + +pw_cc_test( + name = "aes_test", + srcs = [ + "aes_test.cc", + ], + deps = [ + ":aes", + "//pw_containers:vector", + "//pw_unit_test", + ], +) + filegroup( name = "doxygen", srcs = [ diff --git a/pw_crypto/BUILD.gn b/pw_crypto/BUILD.gn index 3ac6fa5b4b..da01b2d5bd 100644 --- a/pw_crypto/BUILD.gn +++ b/pw_crypto/BUILD.gn @@ -81,6 +81,7 @@ pw_size_diff("size_report") { pw_test_group("tests") { tests = [ + ":aes_test", ":sha256_test", ":sha256_mock_test", ":ecdsa_test", @@ -219,3 +220,24 @@ pw_test("ecdsa_test") { deps = [ ":ecdsa" ] sources = [ "ecdsa_test.cc" ] } + +source_set("aes") { + public_configs = [ ":default_config" ] + public = [ + "public/pw_crypto/aes.h", + "public/pw_crypto/aes_backend.h", + "public/pw_crypto/aes_backend_defs.h", + ] + public_deps = [ + "$dir_pw_bytes", + "$dir_pw_status", + ] +} + +pw_test("aes_test") { + deps = [ + ":aes", + "$dir_pw_containers:vector", + ] + sources = [ "aes_test.cc" ] +} diff --git a/pw_crypto/aes_test.cc b/pw_crypto/aes_test.cc new file mode 100644 index 0000000000..3b0ba4f1b5 --- /dev/null +++ b/pw_crypto/aes_test.cc @@ -0,0 +1,127 @@ +// Copyright 2024 The Pigweed Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not +// use this file except in compliance with the License. You may obtain a copy of +// the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations under +// the License. + +#include "pw_crypto/aes.h" + +#include +#include + +#include "pw_assert/assert.h" +#include "pw_containers/vector.h" +#include "pw_status/status.h" +#include "pw_unit_test/framework.h" + +#define EXPECT_OK(expr) EXPECT_EQ(pw::OkStatus(), (expr)) +#define STR_TO_BYTES(str) (as_bytes(span(str).subspan<0, sizeof(str) - 1>())) + +namespace pw::crypto::aes { +// Note: The contents of the `backend` namespace here is a placeholder as this +// test currently only ensures that the facade compiles and can be used +// correctly. +namespace backend { +Status DoEncryptBlock(ConstByteSpan, ConstBlockSpan, BlockSpan) { + return OkStatus(); +} +} // namespace backend + +namespace { + +using backend::AesOperation; +using backend::SupportedKeySize; +using internal::BackendSupports; +using unsafe::aes::EncryptBlock; + +template +void ZeroOut(T& container) { + std::fill(std::begin(container), std::end(container), std::byte{0}); +} + +// Create a view (`span`, as opposed to a `span`) of a `pw::Vector`. +template +span View(pw::Vector& vector) { + return span(vector.begin(), vector.end()); +} + +// Intentionally chosen to not be a valid AES key size, but larger than the +// largest AES key size. +constexpr size_t kMaxVectorSize = 503; + +TEST(Aes, UnsafeEncryptApi) { + constexpr auto kRawEncryptBlockOp = AesOperation::kUnsafeEncryptBlock; + ConstBlockSpan message_block = STR_TO_BYTES("hello, world!\0\0\0"); + + // Ensure various output types work correctly. + std::byte encrypted_carr[16]; + std::array encrypted_arr; + Block encrypted_block; + + // Ensure dynamically-sized keys will work. + Vector dynamic_key; + + auto reset = [&] { + ZeroOut(dynamic_key); + ZeroOut(encrypted_carr); + ZeroOut(encrypted_arr); + ZeroOut(encrypted_block); + + dynamic_key.clear(); + }; + + if constexpr (BackendSupports(SupportedKeySize::k128)) { + span key = STR_TO_BYTES( + "\x13\xA2\x27\x93\x8D\x1D\x89\x46\x07\x4C\xA0\x71\xF2\xF7\x54\xC5"); + + reset(); + + EXPECT_OK(EncryptBlock(key, message_block, encrypted_carr)); + EXPECT_OK(EncryptBlock(key, message_block, encrypted_arr)); + EXPECT_OK(EncryptBlock(key, message_block, encrypted_block)); + + std::copy(key.begin(), key.end(), std::back_inserter(dynamic_key)); + EXPECT_OK(EncryptBlock(View(dynamic_key), message_block, encrypted_block)); + } + + if constexpr (BackendSupports(SupportedKeySize::k192)) { + span key = STR_TO_BYTES( + "\x2B\x43\x70\x51\xBF\x91\xF0\xFD\x4E\x9B\x89\xB7\x35\x40\xD4\x1B" + "\x15\xBC\xD7\xC2\x22\xBC\x03\x76"); + + reset(); + + EXPECT_OK(EncryptBlock(key, message_block, encrypted_carr)); + EXPECT_OK(EncryptBlock(key, message_block, encrypted_arr)); + EXPECT_OK(EncryptBlock(key, message_block, encrypted_block)); + + std::copy(key.begin(), key.end(), std::back_inserter(dynamic_key)); + EXPECT_OK(EncryptBlock(View(dynamic_key), message_block, encrypted_block)); + } + + if constexpr (BackendSupports(SupportedKeySize::k256)) { + span key = STR_TO_BYTES( + "\xA4\xB9\x15\x76\xF2\x16\x67\xB0\x33\x5E\xA6\x8D\xBD\x23\xDF\x29" + "\x84\xBF\x8D\xBE\x56\x77\x13\x28\x14\x55\xD9\x75\xDD\xEE\x4E\x0B"); + + reset(); + + EXPECT_OK(EncryptBlock(key, message_block, encrypted_carr)); + EXPECT_OK(EncryptBlock(key, message_block, encrypted_arr)); + EXPECT_OK(EncryptBlock(key, message_block, encrypted_block)); + + std::copy(key.begin(), key.end(), std::back_inserter(dynamic_key)); + EXPECT_OK(EncryptBlock(View(dynamic_key), message_block, encrypted_block)); + } +} + +} // namespace +} // namespace pw::crypto::aes diff --git a/pw_crypto/docs.rst b/pw_crypto/docs.rst index 44e1450c6c..0e23f1120c 100644 --- a/pw_crypto/docs.rst +++ b/pw_crypto/docs.rst @@ -85,6 +85,27 @@ ECDSA // Handle errors. } +--- +AES +--- + +1. Encrypting a single AES 128-bit block. + +.. warning:: + This is a low-level operation. Users should know exactly what they are doing + and must ensure that this operation does not violate any safety bounds that + more refined operations usually ensure. + +.. code-block:: cpp + + #include "pw_crypto/aes.h" + + std::byte encrypted[16]; + + if (!pw::crypto::unsafe::aes::EncryptBlock(key, message, encrypted).ok()) { + // Handle errors. + } + ------------- Configuration ------------- diff --git a/pw_crypto/public/pw_crypto/aes.h b/pw_crypto/public/pw_crypto/aes.h new file mode 100644 index 0000000000..d3bcb95790 --- /dev/null +++ b/pw_crypto/public/pw_crypto/aes.h @@ -0,0 +1,148 @@ +// Copyright 2024 The Pigweed Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not +// use this file except in compliance with the License. You may obtain a copy of +// the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations under +// the License. + +#pragma once + +#include "pw_assert/assert.h" +#include "pw_bytes/span.h" +#include "pw_crypto/aes_backend.h" +#include "pw_crypto/aes_backend_defs.h" +#include "pw_status/status.h" + +namespace pw::crypto::aes { + +/// Number of bytes in an AES block (16). This is independent of key size. +constexpr size_t kBlockSizeBytes = (128 / 8); +/// Number of bytes in a 128-bit key (16). +constexpr size_t kKey128SizeBytes = (128 / 8); +/// Number of bytes in a 192-bit key (24). +constexpr size_t kKey192SizeBytes = (192 / 8); +/// Number of bytes in a 256-bit key (32). +constexpr size_t kKey256SizeBytes = (256 / 8); + +/// A single AES block. +using Block = std::array; +/// A span of bytes the same size as an AES block. +using BlockSpan = span; +/// A span of const bytes the same size as an AES block. +using ConstBlockSpan = span; + +namespace internal { +/// Utility to get the appropriate `SupportedKeySize` from number of bytes. +constexpr backend::SupportedKeySize FromKeySizeBytes(size_t size) { + switch (size) { + case kKey128SizeBytes: + return backend::SupportedKeySize::k128; + case kKey192SizeBytes: + return backend::SupportedKeySize::k192; + case kKey256SizeBytes: + return backend::SupportedKeySize::k256; + default: + return backend::SupportedKeySize::kUnsupported; + } +} + +template +constexpr bool BackendSupports(backend::SupportedKeySize key_size) { + return (backend::supported & key_size) != + backend::SupportedKeySize::kUnsupported; +} + +/// Utility to determine if an operation supports a particular key size. +template +constexpr bool BackendSupports(size_t key_size_bytes) { + return BackendSupports(FromKeySizeBytes(key_size_bytes)); +} + +} // namespace internal + +namespace backend { +/// Implement `raw::EncryptBlock` in the backend. This function should not be +/// called directly, call `raw::EncryptBlock` instead. +/// +/// @param[in] key A byte string containing the key to use to encrypt the block. +/// The key is guaranteed to be a length that is supported by the backend as +/// declared by `supports`. +/// +/// @param[in] plaintext A 128-bit block of data to encrypt. +/// +/// @param[in] out_ciphertext A 128-bit destination block in which to store the +/// encrypted data. +/// +/// @return @pw_status{OK} for a successful encryption, or an error ``Status`` +/// otherwise. +Status DoEncryptBlock(ConstByteSpan key, + ConstBlockSpan plaintext, + BlockSpan out_ciphertext); +} // namespace backend +} // namespace pw::crypto::aes + +namespace pw::crypto::unsafe::aes { +/// Perform raw block-level AES encryption of a single AES block. +/// +/// @warning This is a low-level operation that should be considered "unsafe" in +/// that users should know exactly what they are doing and must ensure that this +/// operation does not violate any safety bounds that more refined operations +/// usually ensure. +/// +/// Example: +/// +/// @code{.cpp} +/// #include "pw_crypto/aes.h" +/// +/// // Encrypt a single block of data. +/// std::byte encrypted[16]; +/// if (pw::crypto::aes::raw::EncryptBlock(key, message_block, encrypted)) { +/// // handle errors. +/// } +/// @endcode +/// +/// @param[in] key A byte string containing the key to use to encrypt the block. +/// If `key` has a static extent then this will fail to compile if the key size +/// is not supported by the backend. If it has a dynamic extent, then this will +/// fail an assertion at runtime if it is not a supported size. +/// +/// @param[in] plaintext A 128-bit block of data to encrypt. +/// +/// @param[in] out_ciphertext A 128-bit destination block in which to store the +/// encrypted data. +/// +/// @return @pw_status{OK} for a successful encryption, or an error ``Status`` +/// otherwise. +template +inline Status EncryptBlock(span key, + pw::crypto::aes::ConstBlockSpan plaintext, + pw::crypto::aes::BlockSpan out_ciphertext) { + constexpr auto kThisOp = + pw::crypto::aes::backend::AesOperation::kUnsafeEncryptBlock; + static_assert(pw::crypto::aes::internal::BackendSupports(KeySize), + "Unsupported key size for EncryptBlock for backend."); + return pw::crypto::aes::backend::DoEncryptBlock( + key, plaintext, out_ciphertext); +} + +// Specialization for dynamically sized spans. +template <> +inline Status EncryptBlock( + span key, + pw::crypto::aes::ConstBlockSpan plaintext, + pw::crypto::aes::BlockSpan out_ciphertext) { + constexpr auto kThisOp = + pw::crypto::aes::backend::AesOperation::kUnsafeEncryptBlock; + PW_ASSERT(pw::crypto::aes::internal::BackendSupports(key.size())); + return pw::crypto::aes::backend::DoEncryptBlock( + key, plaintext, out_ciphertext); +} + +} // namespace pw::crypto::unsafe::aes diff --git a/pw_crypto/public/pw_crypto/aes_backend.h b/pw_crypto/public/pw_crypto/aes_backend.h new file mode 100644 index 0000000000..073b1636e2 --- /dev/null +++ b/pw_crypto/public/pw_crypto/aes_backend.h @@ -0,0 +1,27 @@ +// Copyright 2024 The Pigweed Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not +// use this file except in compliance with the License. You may obtain a copy of +// the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations under +// the License. + +#pragma once + +#include "pw_crypto/aes_backend_defs.h" + +// Note: This is a fake backend, only used to ensure tests compile. This will be +// removed once a backend is implemented. + +namespace pw::crypto::aes::backend { +// The fake backend supports 128-bit and 256-bit keys. +template <> +inline constexpr SupportedKeySize supported = + SupportedKeySize::k128 | SupportedKeySize::k192 | SupportedKeySize::k256; +} // namespace pw::crypto::aes::backend diff --git a/pw_crypto/public/pw_crypto/aes_backend_defs.h b/pw_crypto/public/pw_crypto/aes_backend_defs.h new file mode 100644 index 0000000000..0f7856123d --- /dev/null +++ b/pw_crypto/public/pw_crypto/aes_backend_defs.h @@ -0,0 +1,79 @@ +// Copyright 2024 The Pigweed Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not +// use this file except in compliance with the License. You may obtain a copy of +// the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations under +// the License. + +#pragma once + +#include +#include +#include + +namespace pw::crypto::aes::backend { +/// Possible supported AES operations. See `supports` for details. +enum class AesOperation : uint64_t { + kUnsafeEncryptBlock, +}; + +/// Possible supported key sizes. See `supports` for details. +enum class SupportedKeySize : uint8_t { + /// The operation is entirely unsupported for any key size. + kUnsupported = 0, + /// The operation supports 128-bit keys. + k128 = 1 << 0, + /// The operation supports 192-bit keys. + k192 = 1 << 1, + /// The operation supports 256-bit keys. + k256 = 1 << 2, +}; + +/// Support bitwise & for `SupportedKeySize`. +constexpr SupportedKeySize operator&(SupportedKeySize x, SupportedKeySize y) { + return static_cast( + static_cast::type>(x) & + static_cast::type>(y)); +} + +/// Support bitwise | for `SupportedKeySize`. +constexpr SupportedKeySize operator|(SupportedKeySize x, SupportedKeySize y) { + return static_cast( + static_cast::type>(x) | + static_cast::type>(y)); +} + +/// Support bitwise ^ for `SupportedKeySize`. +constexpr SupportedKeySize operator^(SupportedKeySize x, SupportedKeySize y) { + return static_cast( + static_cast::type>(x) | + static_cast::type>(y)); +} + +/// A backend must declare which operations it supports, and which key sizes it +/// supports for those operations. This declaration must be made in the +/// `pw::crypto::aes::backend` namespace in the header provided by the backend, +/// and it is always valid to override this for any `AesOperation`. For example, +/// to declare that the backend supports the operation +///`pw::crypto::aes::raw::EncryptBlock` with both 128-bit and 256-bit keys, do: +/// +/// @code{.cpp} +/// namespace pw::crypto::aes::backend { +/// template<> inline constexpr SupportedKeySizes supported = +/// SupportedKeySize::k128 | SupportedKeySize::k256; +/// } +/// @endcode +/// +/// By default all operations are unsupported for all key sizes, so a backend +/// must explicitly decleare that an operation is supported and which key sizes +/// it supports. +template +inline constexpr SupportedKeySize supported = SupportedKeySize::kUnsupported; +} // namespace pw::crypto::aes::backend