Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions doc/api_ref/cipher_modes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -454,6 +454,26 @@ Algorithm specification name:
- Tag size defaults to 16.
- Examples: e.g. ``AES-128/GCM``, ``AES-128/GCM(12)``

GCM-SIV
~~~~~~~~

Available if ``BOTAN_HAS_AEAD_GCM_SIV`` is defined.

AES-GCM-SIV, specified in RFC 8452. Like SIV this mode is resistant to nonce
misuse; if a nonce is ever reused, the only information leaked is if two
messages encrypted under the same nonce were identical. Requires a 128-bit
block cipher with either a 128-bit or 256-bit key. The nonce must be exactly
96 bits, and the tag is always 128 bits.

Note that unlike SIV, GCM-SIV is not usable as a deterministic (nonce-less)
encryption scheme; a nonce must always be provided.

Algorithm specification name:
``<BlockCipher>/GCM-SIV`` (reported name) /
``GCM-SIV(<BlockCipher>)``

- Examples: ``AES-128/GCM-SIV``, ``AES-256/GCM-SIV``

OCB
~~~~~

Expand Down
1 change: 0 additions & 1 deletion doc/dev_ref/todo.rst
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ Request a new feature by opening a pull request to update this file.

New Ciphers/Hashes/MACs
----------------------------------------
* GCM-SIV (RFC 8452)
* EME* tweakable block cipher (https://eprint.iacr.org/2004/125)
* PMAC
* SIV-PMAC
Expand Down
18 changes: 18 additions & 0 deletions src/lib/modes/aead/aead.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@
#include <botan/internal/gcm.h>
#endif

#if defined(BOTAN_HAS_AEAD_GCM_SIV)
#include <botan/internal/gcm_siv.h>
#endif

#if defined(BOTAN_HAS_AEAD_OCB)
#include <botan/internal/ocb.h>
#endif
Expand Down Expand Up @@ -141,6 +145,20 @@ std::unique_ptr<AEAD_Mode> AEAD_Mode::create(std::string_view algo, Cipher_Dir d
}
#endif

#if defined(BOTAN_HAS_AEAD_GCM_SIV)
if(req.algo_name() == "GCM-SIV") {
// Unlike GCM the tag length is fixed, so reject eg "AES-128/GCM-SIV(12)"
if(req.arg_count() != 1) {
return std::unique_ptr<AEAD_Mode>();
}
if(dir == Cipher_Dir::Encryption) {
return std::make_unique<GCM_SIV_Encryption>(std::move(bc));
} else {
return std::make_unique<GCM_SIV_Decryption>(std::move(bc));
}
}
#endif

#if defined(BOTAN_HAS_AEAD_OCB)
if(req.algo_name() == "OCB") {
const size_t tag_len = req.arg_as_integer(1, 16);
Expand Down
287 changes: 287 additions & 0 deletions src/lib/modes/aead/gcm_siv/gcm_siv.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,287 @@
/*
* GCM-SIV Mode (RFC 8452)
* (C) 2026 Jack Lloyd
*
* Botan is released under the Simplified BSD License (see license.txt)
*/

#include <botan/internal/gcm_siv.h>

#include <botan/exceptn.h>
#include <botan/mem_ops.h>
#include <botan/internal/ct_utils.h>
#include <botan/internal/fmt.h>
#include <botan/internal/int_utils.h>
#include <botan/internal/loadstor.h>

namespace Botan {

namespace {

Key_Length_Specification gcm_siv_key_spec(const BlockCipher& cipher) {
if(cipher.block_size() != 16) {
throw Invalid_Argument("GCM-SIV requires a 128 bit block cipher");
}

// The key-generating key is either 128 or 256 bits, and the derived
// message-encryption key is of the same length
const bool k16 = cipher.valid_keylength(16);
const bool k32 = cipher.valid_keylength(32);

if(k16 && k32) {
return Key_Length_Specification(16, 32, 16);
} else if(k16) {
return Key_Length_Specification(16);
} else if(k32) {
return Key_Length_Specification(32);
} else {
throw Invalid_Argument("GCM-SIV requires a cipher supporting 128 or 256 bit keys");
}
}

} // namespace

GCM_SIV_Mode::GCM_SIV_Mode(std::unique_ptr<BlockCipher> cipher) :
m_cipher_name(cipher->name()),
m_key_spec(gcm_siv_key_spec(*cipher)),
m_cipher(std::move(cipher)),
m_msg_cipher(m_cipher->new_object()) {}

GCM_SIV_Mode::~GCM_SIV_Mode() = default;

void GCM_SIV_Mode::clear() {
m_cipher->clear();
m_kgk_len = 0;
zap(m_ad);
reset();
}

void GCM_SIV_Mode::reset() {
// The derived keys are per-message; start_msg rekeys both
m_msg_cipher->clear();
m_polyval.clear();
secure_scrub_memory(m_nonce);
m_msg_buf.clear();
m_in_msg = false;
}

std::string GCM_SIV_Mode::name() const {
return fmt("{}/GCM-SIV", m_cipher_name);
}

std::string GCM_SIV_Mode::provider() const {
return m_polyval.provider();
}

size_t GCM_SIV_Mode::update_granularity() const {
return 1;
}

size_t GCM_SIV_Mode::ideal_granularity() const {
return BS * std::max<size_t>(2, BlockCipher::ParallelismMult);
}

bool GCM_SIV_Mode::valid_nonce_length(size_t len) const {
return len == 12;
}

Key_Length_Specification GCM_SIV_Mode::key_spec() const {
return m_key_spec;
}

bool GCM_SIV_Mode::has_keying_material() const {
return m_cipher->has_keying_material();
}

void GCM_SIV_Mode::key_schedule(std::span<const uint8_t> key) {
m_cipher->set_key(key);
m_kgk_len = key.size();
reset();
}

void GCM_SIV_Mode::set_associated_data_n(size_t idx, std::span<const uint8_t> ad) {
BOTAN_ARG_CHECK(idx == 0, "GCM-SIV: cannot handle non-zero index in set_associated_data_n");
BOTAN_STATE_CHECK(!m_in_msg);
BOTAN_ARG_CHECK(static_cast<uint64_t>(ad.size()) <= MAX_INPUT_LEN, "GCM-SIV AD too large");
m_ad.assign(ad.begin(), ad.end());
}

void GCM_SIV_Mode::start_msg(const uint8_t nonce[], size_t nonce_len) {
assert_key_material_set();
BOTAN_STATE_CHECK(!m_in_msg);

if(!valid_nonce_length(nonce_len)) {
throw Invalid_IV_Length(name(), nonce_len);
}

copy_mem(m_nonce, std::span{nonce, nonce_len});

/*
Derive the per-nonce keys, RFC 8452 section 4:

"These keys are generated by encrypting a series of plaintext blocks
that contain a 32-bit, little-endian counter followed by the nonce,
and then discarding the second half of the resulting ciphertext."
*/
const size_t blocks = (m_kgk_len == 16) ? 4 : 6;

std::array<uint8_t, 6 * BS> kb{};
for(size_t i = 0; i != blocks; ++i) {
store_le(static_cast<uint32_t>(i), &kb[BS * i]);
copy_mem(&kb[BS * i + 4], m_nonce.data(), m_nonce.size());
}
m_cipher->encrypt_n(kb.data(), kb.data(), blocks);

std::array<uint8_t, 16> auth_key{};
secure_vector<uint8_t> enc_key(m_kgk_len);

for(size_t i = 0; i != 2; ++i) {
copy_mem(&auth_key[8 * i], &kb[BS * i], 8);
}
for(size_t i = 0; i != blocks - 2; ++i) {
copy_mem(&enc_key[8 * i], &kb[BS * (i + 2)], 8);
}

m_polyval.set_key(auth_key);
m_msg_cipher->set_key(enc_key);

secure_scrub_memory(kb);
secure_scrub_memory(auth_key);

m_msg_buf.clear();
m_in_msg = true;
}

size_t GCM_SIV_Mode::process_msg(uint8_t buf[], size_t sz) {
BOTAN_STATE_CHECK(m_in_msg);

// Early rejection to bound buffering; the exact (per-direction) limits
// are checked in finish. The tag is included to allow decryption inputs.
if(sz > MAX_INPUT_LEN + BS - m_msg_buf.size()) {
throw Invalid_State("GCM-SIV message length limit exceeded");
}

// All input is buffered until finish
m_msg_buf.insert(m_msg_buf.end(), buf, buf + sz);
return 0;
}

std::array<uint8_t, GCM_SIV_Mode::BS> GCM_SIV_Mode::compute_tag(std::span<const uint8_t> ptext) {
m_polyval.update(m_ad);
m_polyval.zero_pad();
m_polyval.update(ptext);
m_polyval.zero_pad();

const uint64_t ad_bits = 8 * static_cast<uint64_t>(m_ad.size());
const uint64_t pt_bits = 8 * static_cast<uint64_t>(ptext.size());
m_polyval.update(store_le(ad_bits, pt_bits));

std::array<uint8_t, BS> S{};
m_polyval.final(S);

/*
RFC 8452 section 4: "XOR the first twelve bytes of S_s with the nonce
and clear the most significant bit of the last byte. Encrypt the
result with AES using the message-encryption key to produce the tag."
*/
xor_buf(S.data(), m_nonce.data(), m_nonce.size());
S[15] &= 0x7f;
m_msg_cipher->encrypt(S);

return S;
}

void GCM_SIV_Mode::ctr_xor(std::span<const uint8_t, BS> tag, uint8_t buf[], size_t len) {
/*
RFC 8452 section 4: "The initial counter block is the tag with the
most significant bit of the last byte set to one. The counter
advances by incrementing the first 32 bits interpreted as an
unsigned, little-endian integer, wrapping at 2^32."
*/
std::array<uint8_t, BS> ctr_block{};
copy_mem(ctr_block, tag);
ctr_block[15] |= 0x80;

uint32_t ctr32 = load_le<uint32_t>(ctr_block.data(), 0);

secure_vector<uint8_t> ks(m_msg_cipher->parallel_bytes());

while(len > 0) {
const size_t blocks = std::min((len + BS - 1) / BS, ks.size() / BS);

for(size_t i = 0; i != blocks; ++i) {
copy_mem(&ks[BS * i], ctr_block.data(), BS);
store_le(ctr32, &ks[BS * i]);
ctr32 += 1;
}

m_msg_cipher->encrypt_n(ks.data(), ks.data(), blocks);

const size_t todo = std::min(len, blocks * BS);
xor_buf(buf, ks.data(), todo);
buf += todo;
len -= todo;
}
}

size_t GCM_SIV_Encryption::output_length(size_t input_length) const {
return add_or_throw(input_length, tag_size(), "GCM-SIV input too large");
}

void GCM_SIV_Encryption::finish_msg(secure_vector<uint8_t>& buffer, size_t offset) {
BOTAN_STATE_CHECK(in_msg());
BOTAN_ARG_CHECK(offset <= buffer.size(), "Invalid offset");

buffer.insert(buffer.begin() + offset, msg_buf().begin(), msg_buf().end());
msg_buf().clear();

const size_t ptext_len = buffer.size() - offset;
BOTAN_ARG_CHECK(static_cast<uint64_t>(ptext_len) <= MAX_INPUT_LEN, "GCM-SIV plaintext too large");
uint8_t* buf = buffer.data() + offset;

const auto tag = compute_tag({buf, ptext_len});
ctr_xor(tag, buf, ptext_len);

buffer += std::make_pair(tag.data(), tag.size());
reset();
}

size_t GCM_SIV_Decryption::output_length(size_t input_length) const {
BOTAN_ARG_CHECK(input_length >= tag_size(), "Message too short to be valid");
return input_length - tag_size();
}

void GCM_SIV_Decryption::finish_msg(secure_vector<uint8_t>& buffer, size_t offset) {
BOTAN_STATE_CHECK(in_msg());
BOTAN_ARG_CHECK(offset <= buffer.size(), "Invalid offset");

if(!msg_buf().empty()) {
buffer.insert(buffer.begin() + offset, msg_buf().begin(), msg_buf().end());
msg_buf().clear();
}

const size_t sz = buffer.size() - offset;
BOTAN_ARG_CHECK(sz >= tag_size(), "input did not include the tag");

const size_t ctext_len = sz - tag_size();
BOTAN_ARG_CHECK(static_cast<uint64_t>(ctext_len) <= MAX_INPUT_LEN, "GCM-SIV ciphertext too large");
uint8_t* buf = buffer.data() + offset;

std::array<uint8_t, 16> included_tag{};
copy_mem(included_tag, std::span{buf + ctext_len, 16});

ctr_xor(included_tag, buf, ctext_len);

const auto expected_tag = compute_tag({buf, ctext_len});

reset();

if(!CT::is_equal(expected_tag.data(), included_tag.data(), included_tag.size()).as_bool()) {
clear_mem(std::span{buffer}.subspan(offset, ctext_len));
throw Invalid_Authentication_Tag("GCM-SIV tag check failed");
}

buffer.resize(offset + ctext_len);
}

} // namespace Botan
Loading
Loading