Thursday, October 15, 2026
HomeC ProgrammingSerpent Cipher in C, C++, Java, Python and C#: How It Works

Serpent Cipher in C, C++, Java, Python and C#: How It Works


Serpent encrypts each block with 32 rounds. The very best printed key-recovery assault reaches 12 of them, and wishes extra recognized plaintext than anybody might gather. That margin was the purpose of the design: Serpent was constructed to be conservative, and it completed second within the vote that selected the Superior Encryption Commonplace, 59 votes to Rijndael’s 86.

This information explains how Serpent works, from its eight 4-bit S-boxes and bitslice spherical construction to its key schedule, and implements it in C, C++, Java, Python and C#. It additionally covers the byte-order entice that makes two appropriate Serpent implementations disagree, 4 errors {that a} fast check won’t catch, and how briskly a readable implementation actually is. Each program was compiled and run for this text on Ubuntu 24.04 (x86-64): C with GCC 13.3 and Clang 18.1.3 underneath -std=c11 and -std=c17, C++ with g++ 13.3 and clang++ 18.1.3 underneath -std=c++17 and -std=c++20, all with -Wall -Wextra -pedantic -Werror; Java on OpenJDK 21 with --release 17 -Xlint:all -Werror; Python 3.13 with ruff and mypy --strict clear; and C# on the .NET 10 SDK with analyzer warnings as errors. All 5 cross the identical 1,043 printed check vectors and print byte-identical output, the C and C++ exams run clear underneath AddressSanitizer and UndefinedBehaviorSanitizer, and each output block is captured verbatim. The code is in 5 GitHub repositories whose workflows repeat these checks on every commit, together with MSVC, Home windows and macOS jobs that weren’t run on the check machine.

What Is the Serpent Cipher?

Serpent is a symmetric block cipher that encrypts 128-bit blocks with a key of as much as 256 bits. It’s a substitution–permutation community of 32 rounds; every spherical mixes in a 128-bit subkey, passes the block by certainly one of eight 4-bit S-boxes, and stirs the consequence with a linear transformation. Serpent was a finalist for the Superior Encryption Commonplace and is within the public area.

Ross Anderson, Eli Biham and Lars Knudsen designed Serpent and submitted it to the AES competitors in 1998. It was one of many 5 finalists, with MARS, RC6, Rijndael and Twofish. On the final AES candidate convention Rijndael obtained 86 votes and Serpent 59, because the Serpent residence web page information, and NIST introduced Rijndael because the winner on October 2, 2000. The designers positioned Serpent within the public area, so anybody might use it with no licence. It’s nonetheless provided at this time, for instance as one of many quantity ciphers in VeraCrypt. For a classical cipher to match it with, see the Vigenère cipher in C++, which a pc can break in seconds.

Property Serpent
Block measurement 128 bits (16 bytes)
Key measurement Any size as much as 256 bits; 128, 192 and 256 are the usual sizes
Rounds 32
S-boxes 8 completely different 4-bit S-boxes, every utilized in 4 of the 32 rounds
Subkeys 33 subkeys of 128 bits, K0 to K32
Construction Substitution–permutation community
Licence Public area

How Serpent Works

Serpent treats the 16-byte block as 4 32-bit phrases, x0 to x3. A spherical does three issues:

  1. XOR the spherical’s subkey into the 4 phrases.
  2. Substitute. Take bit j of every of the 4 phrases; collectively they type a 4-bit quantity. Exchange it with its entry within the spherical’s S-box, and write the 4 output bits again to place j. Do that for all 32 bit positions. Spherical r makes use of S-box r mod 8.
  3. Combine the 4 phrases with the linear transformation: mounted rotations, shifts and XORs that unfold each output bit of 1 spherical throughout many S-boxes of the subsequent.

Rounds 0 to 30 comply with that sample. Spherical 31 replaces the linear transformation with an XOR of the final subkey, K32, so the cipher each begins and ends with key materials.

One Serpent spherical, in bitslice type The 128-bit block is 4 32-bit phrases. Every S-box enter is one bit from every phrase. bit 31 bit 0 x0 x1 x2 x3 low bit excessive bit 1 0 (spherical 0 enter of the demo: plaintext XOR K0) Column 5, learn from x3 all the way down to x0: 1110 = 14 S0[14] = 9 = 1001, written again into column 5 The identical S-box runs on all 32 columns. Spherical r makes use of S-box S(r mod 8). XOR with subkey Kr S-box S(r mod 8) on all 32 columns Linear rework rotate, shift, XOR rounds 0 to 30: 31 instances Spherical 31: XOR K31, S-box S7, then XOR K32 rather than the linear rework Decryption runs the identical steps in reverse with the inverse S-boxes and inverse rework.
One spherical of Serpent in bitslice type, drawn from the demo’s actual round-0 enter. Bit 5 of the 4 phrases varieties the S-box enter 14, and S-box S0 maps it to 9.

Arranging the block this manner known as bitslicing. The 32 S-box lookups of a spherical all learn bit j from the identical 4 phrases, so an implementation can compute all 32 of them directly with a brief sequence of AND, OR, XOR and NOT directions on entire phrases. Serpent was designed for precisely that. The specification additionally describes the cipher with an preliminary and a closing bit permutation round a traditional structure; that description offers the identical ciphertext, and the bitslice type is the one implementations use.

The code under favours readability over velocity. Every S-box is a 16-entry desk, and the substitution loops over the 32 bit positions, so you may see the bitslice construction immediately. “How Quick Is It?” additional down measures what that prices.

The Eight S-Bins

/* The eight 4-bit S-boxes from the Serpent specification. */
static const uint8_t SBOX[8][16] = {
    { 3, 8, 15, 1, 10, 6, 5, 11, 14, 13, 4, 2, 7, 0, 9, 12 },
    { 15, 12, 2, 7, 9, 0, 5, 10, 1, 11, 14, 8, 6, 13, 3, 4 },
    { 8, 6, 7, 9, 3, 12, 10, 15, 13, 1, 14, 4, 0, 11, 5, 2 },
    { 0, 15, 11, 8, 12, 9, 6, 3, 13, 1, 2, 4, 10, 7, 5, 14 },
    { 1, 15, 8, 3, 12, 0, 11, 6, 2, 5, 4, 10, 9, 14, 7, 13 },
    { 15, 5, 2, 11, 4, 10, 9, 12, 0, 3, 14, 8, 13, 6, 7, 1 },
    { 7, 2, 12, 5, 8, 4, 6, 11, 14, 9, 1, 15, 13, 3, 10, 0 },
    { 1, 13, 15, 0, 14, 8, 2, 11, 7, 4, 12, 10, 9, 3, 5, 6 },
};

Every row is a permutation of 0 to fifteen, so each S-box might be inverted, and decryption makes use of the eight inverse tables. The C file lists them too; the C++ model computes them from these tables at compile time, and the Java, Python and C# variations at start-up, which leaves one much less desk to mistype.

The Bitslice Substitution

/* Bitslice S-box: bit j of x[0..3] varieties a 4-bit enter, x[0] the low bit.
 * All 32 bit positions undergo the identical 4-bit desk. */
static void apply_sbox(const uint8_t field[16], uint32_t x[4])
{
    uint32_t y[4] = { 0, 0, 0, 0 };
    for (unsigned j = 0; j < 32; ++j)  ((x[1] >> j) & 1u) << 1 
    memcpy(x, y, sizeof y);
}

The enter nibble takes its low bit from x[0] and its excessive bit from x[3]. Getting that order backwards nonetheless produces a working cipher that decrypts its personal output, which is why the check vectors matter (see “4 Errors That Break a Serpent Implementation”).

The Linear Transformation

/* The linear transformation that mixes the 4 phrases after every S-box. */
static void linear_transform(uint32_t x[4])
{
    x[0] = rotl(x[0], 13);
    x[2] = rotl(x[2], 3);
    x[1] ^= x[0] ^ x[2];
    x[3] ^= x[2] ^ (x[0] << 3);
    x[1] = rotl(x[1], 1);
    x[3] = rotl(x[3], 7);
    x[0] ^= x[1] ^ x[3];
    x[2] ^= x[3] ^ (x[1] << 7);
    x[0] = rotl(x[0], 5);
    x[2] = rotl(x[2], 22);
}

The rotations by 13, 3, 1, 7, 5 and 22 bits transfer every phrase’s bits to new positions, and the 2 plain shifts (<< 3 and << 7) are intentionally not rotations: they drop the bits that fall off the highest. Decryption runs the identical ten steps backwards, with proper rotations rather than left ones. The 2 strains with shifts want no inverse operation of their very own, as a result of every XORs in a price computed from phrases that step leaves unchanged.

The Key Schedule

int serpent_set_key(serpent_key *ks, const uint8_t *key, size_t key_len)
{
    if (key_len == 0 || key_len > SERPENT_MAX_KEY_SIZE)
        return -1;

    /* Pad the important thing to 256 bits: one 1 bit after the important thing, then zeros. */
    uint8_t padded[SERPENT_MAX_KEY_SIZE] = { 0 };
    memcpy(padded, key, key_len);
    if (key_len < SERPENT_MAX_KEY_SIZE)
        padded[key_len] = 0x01;

    /* w[0..7] maintain the padded key; w[8..139] are the 132 prekey phrases. */
    uint32_t w[140];
    for (unsigned i = 0; i < 8; ++i)
        w[i] = load_le32(padded + 4 * i);
    for (unsigned i = 8; i < 140; ++i)
        w[i] = rotl(w[i - 8] ^ w[i - 5] ^ w[i - 3] ^ w[i - 1] ^ PHI ^ (uint32_t)(i - 8), 11);

    /* Subkey i passes prekeys 4i..4i+3 by S-box (3 - i) mod 8. */
    for (unsigned i = 0; i < 33; ++i) {
        uint32_t *okay = ks->subkey[i];
        memcpy(okay, &w[8 + 4 * i], 4 * sizeof(uint32_t));
        apply_sbox(SBOX[(3 - i) & 7], okay);
    }

    memset(padded, 0, sizeof padded);
    memset(w, 0, sizeof w);
    return 0;
}

Each key’s first padded to 256 bits: a single 1 bit straight after the important thing, then zeros. The eight padded phrases then develop into 132 prekeys with the recurrence wᵢ = (wᵢ₋₈ ⊕ wᵢ₋₅ ⊕ wᵢ₋₃ ⊕ wᵢ₋₁ ⊕ φ ⊕ i) ⋘ 11, the place φ = 0x9e3779b9 is the fractional a part of the golden ratio and that i counts the prekeys from 0. Teams of 4 prekeys cross by the S-boxes, within the order S3, S2, S1, S0, S7, S6 and so forth, to provide the 33 subkeys. The expanded key, 528 bytes, is computed as soon as and reused for each block.

Encrypting and Decrypting a Block

void serpent_encrypt(const serpent_key *ks, const uint8_t in[SERPENT_BLOCK_SIZE],
                     uint8_t out[SERPENT_BLOCK_SIZE])
{
    uint32_t x[4];
    for (unsigned i = 0; i < 4; ++i)
        x[i] = load_le32(in + 4 * i);

    for (unsigned r = 0; r < 32; ++r) {
        xor_key(x, ks->subkey[r]);
        apply_sbox(SBOX[r & 7], x);
        if (r < 31)
            linear_transform(x);
        else
            xor_key(x, ks->subkey[32]);   /* the final spherical replaces LT with K32 */
    }

    for (unsigned i = 0; i < 4; ++i)
        store_le32(out + 4 * i, x[i]);
}

void serpent_decrypt(const serpent_key *ks, const uint8_t in[SERPENT_BLOCK_SIZE],
                     uint8_t out[SERPENT_BLOCK_SIZE])
{
    uint32_t x[4];
    for (unsigned i = 0; i < 4; ++i)
        x[i] = load_le32(in + 4 * i);

    for (int r = 31; r >= 0; --r) {
        if (r == 31)
            xor_key(x, ks->subkey[32]);
        else
            inverse_linear_transform(x);
        apply_sbox(SBOX_INV[r & 7], x);
        xor_key(x, ks->subkey[r]);
    }

    for (unsigned i = 0; i < 4; ++i)
        store_le32(out + 4 * i, x[i]);
}

Phrases are learn little-endian: byte 0 of the block is the bottom byte of x0. That is the byte order of the NESSIE check vectors, which libgcrypt and Botan comply with. A second, equally legitimate conference exists; the byte-order part under explains it.

A Full Instance in C

/* serpent_demo.c - encrypts and decrypts one block with a 256-bit key, then
 * checks two printed check vectors: one in NESSIE byte order, and one from
 * the Serpent AES submission, which writes each worth byte-reversed.
 * Construct: gcc -std=c11 -Wall -Wextra -pedantic serpent_demo.c serpent.c -o serpent_demo
 */
#embrace <stdio.h>
#embrace <string.h>

#embrace "serpent.h"

static void print_hex(const char *label, const uint8_t *p, size_t n)
{
    printf("%-11s", label);
    for (size_t i = 0; i < n; ++i)
        printf("%02x", p[i]);
    printf("n");
}

static void reverse(uint8_t *p, size_t n)
{
    for (size_t i = 0; i < n / 2; ++i) {
        uint8_t t = p[i];
        p[i] = p[n - 1 - i];
        p[n - 1 - i] = t;
    }
}

int important(void)
{
    uint8_t key[32];
    for (size_t i = 0; i < sizeof key; ++i)
        key[i] = (uint8_t)i;                         /* 00 01 02 ... 1f */
    static const char textual content[] = "Serpent: 128-bit";   /* 16 characters + NUL */
    uint8_t plain[16];
    memcpy(plain, textual content, sizeof plain);                /* the block is the 16 characters */

    serpent_key ks;
    if (serpent_set_key(&ks, key, sizeof key) != 0)
        return 1;
    uint8_t cipher[16], again[16];
    serpent_encrypt(&ks, plain, cipher);
    serpent_decrypt(&ks, cipher, again);
    serpent_wipe_key(&ks);

    print_hex("key", key, sizeof key);
    print_hex("plaintext", plain, sizeof plain);
    print_hex("ciphertext", cipher, sizeof cipher);
    printf("decrypts again: %sn", memcmp(again, plain, 16) == 0 ? "sure" : "no");

    /* NESSIE: 128-bit key 80 00 .. 00, zero block -> 264e5481... */
    uint8_t k128[16] = { 0x80 }, zero[16] = { 0 }, out[16];
    const uint8_t nessie[16] = { 0x26, 0x4e, 0x54, 0x81, 0xef, 0xf4, 0x2a, 0x46,
                                 0x06, 0xab, 0xda, 0x06, 0xc0, 0xbf, 0xda, 0x3d };
    serpent_set_key(&ks, k128, sizeof k128);
    serpent_encrypt(&ks, zero, out);
    printf("NESSIE vector:          %sn", memcmp(out, nessie, 16) == 0 ? "match" : "MISMATCH");

    /* AES submission: 256-bit zero key, zero block -> 89104945... as soon as the
     * output bytes are reversed (the important thing and block are all zero both approach). */
    const uint8_t submission[16] = { 0x89, 0x10, 0x49, 0x45, 0x04, 0x18, 0x19, 0x50,
                                     0xf9, 0x8d, 0xd9, 0x98, 0xa8, 0x2b, 0x67, 0x49 };
    uint8_t k256[32] = { 0 };
    serpent_set_key(&ks, k256, sizeof k256);
    serpent_encrypt(&ks, zero, out);
    reverse(out, sizeof out);
    printf("AES submission vector:  %sn", memcmp(out, submission, 16) == 0 ? "match" : "MISMATCH");
    serpent_wipe_key(&ks);
    return 0;
}

Output:

key        000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f
plaintext  53657270656e743a203132382d626974
ciphertext 8f9d6a59c08901a421126fcffc13c5f6
decrypts again: sure
NESSIE vector:          match
AES submission vector:  match

The ciphertext line was additionally produced, independently, by libgcrypt’s Serpent with the identical key and block. The 2 test strains are known-answer exams from two printed sources, and the subsequent part explains why the second wants reverse().

Byte Order: Why Two Right Serpent Implementations Disagree

Serpent’s AES submission wrote keys, blocks and ciphertexts as hexadecimal numbers with probably the most vital digit first. The NESSIE challenge, which printed the check vectors most libraries now use, wrote them as byte strings in reminiscence order. The cipher is similar; the bytes are listed in reverse orders. For a 256-bit all-zero key and an all-zero block:

Conference Ciphertext as printed
NESSIE order (libgcrypt, Botan, the code on this web page) 49672ba898d98df95019180445491089
AES submission order 8910494504181950f98dd998a82b6749

Every string is the opposite one backwards, byte by byte. To transform, reverse all 16 bytes of the plaintext, reverse the important thing bytes, encrypt, and reverse the 16 output bytes. Bouncy Fortress ships the submission-order variant as a separate cipher named TnepresEngine, “Serpent” spelled backwards, and its 13 check vectors all matched the code on this web page after that reversal.

In case your Serpent output seems like a scrambled model of a printed vector, test the byte order earlier than you test anything. And when two methods should trade Serpent ciphertext, agree on the conference explicitly; the cipher’s title alone doesn’t settle it.

Serpent in C++, Java, Python and C#

Every of the 4 implementations under follows the C code step for step, prints precisely the output above, and passes the identical 1,043 check vectors. These excerpts present what every language modifications; the whole information are within the repositories linked in “Supply Code and Checks”.

Serpent in C++

class Serpent {
public:
    static constexpr std::size_t block_size = 16;
    static constexpr std::size_t max_key_size = 32;
    utilizing Block = std::array<std::uint8_t, block_size>;

    // Expands a key of 1 to 32 bytes; throws std::invalid_argument in any other case.
    Serpent(const std::uint8_t* key, std::size_t key_len);

    // Any contiguous byte container: std::array, std::vector, std::string_view...
    template <class Bytes>
    express Serpent(const Bytes& key)
        : Serpent(reinterpret_cast<const std::uint8_t*>(std::knowledge(key)), std::measurement(key)) {}

    ~Serpent();                                  // wipes the subkeys
    Serpent(const Serpent&) = default;
    Serpent& operator=(const Serpent&) = default;

    [[nodiscard]] Block encrypt(const Block& in) const;
    [[nodiscard]] Block decrypt(const Block& in) const;

personal:
    utilizing Phrases = std::array<std::uint32_t, 4>;
    std::array<Phrases, 33> subkeys_{};            // K0..K32
};

The C++ model is a header-only class in namespace mycplus. Block is a std::array<std::uint8_t, 16>, so a block can’t be the improper size, and the templated constructor accepts any contiguous byte container. encrypt and decrypt are [[nodiscard]], as a result of discarding a ciphertext is at all times a bug, and the destructor overwrites the subkeys. A key of the improper size throws std::invalid_argument. The inverse S-boxes are constructed by the compiler:

struct InverseSboxes {
    std::uint8_t field[8][16]{};
    constexpr InverseSboxes() {
        for (int s = 0; s < 8; ++s)
            for (int i = 0; i < 16; ++i)
                field[s][sbox[s][i]] = static_cast<std::uint8_t>(i);
    }
};
inline constexpr InverseSboxes sbox_inv{};

Utilization reads naturally:

    const Serpent cipher(key);
    const Serpent::Block encrypted = cipher.encrypt(plain);
    const Serpent::Block again = cipher.decrypt(encrypted);

Serpent in Java

    /** Expands a key of 1 to 32 bytes; shorter keys are padded because the specification requires. */
    public Serpent(byte[] key) {
        if (key == null || key.size == 0 || key.size > MAX_KEY_SIZE) {
            throw new IllegalArgumentException("Serpent keys are 1 to 32 bytes");
        }
        // Pad to 256 bits: one 1 bit after the important thing, then zeros.
        byte[] padded = Arrays.copyOf(key, MAX_KEY_SIZE);
        if (key.size < MAX_KEY_SIZE) {
            padded[key.length] = 1;
        }
        // w[0..7] maintain the padded key; w[8..139] are the 132 prekey phrases.
        int[] w = new int[140];
        for (int i = 0; i < 8; i++) {
            w[i] = loadLe32(padded, 4 * i);
        }
        for (int i = 8; i < 140; i++) {
            w[i] = Integer.rotateLeft(w[i - 8] ^ w[i - 5] ^ w[i - 3] ^ w[i - 1] ^ PHI ^ (i - 8), 11);
        }
        // Subkey i passes prekeys 4i..4i+3 by S-box (3 - i) mod 8.
        for (int i = 0; i < 33; i++) {
            System.arraycopy(w, 8 + 4 * i, subkeys[i], 0, 4);
            applySbox(SBOX[Math.floorMod(3 - i, 8)], subkeys[i]);
        }
        Arrays.fill(padded, (byte) 0);
        Arrays.fill(w, 0);
    }

Java has no unsigned 32-bit kind, however Serpent wants solely XOR, shifts and rotations, which give the identical bits on a signed int. Two particulars elsewhere within the file matter: the S-box loop makes use of >>>, the unsigned shift, and the byte loader masks every byte with & 0xff, as a result of Java’s byte is signed. Integer.rotateLeft and Integer.rotateRight provide the rotations, and Math.floorMod(3 - i, 8) retains the S-box index non-negative. The category’s wipe() methodology clears the subkeys, though a rubbish collector that strikes objects might depart older copies in reminiscence, which Java offers no method to clear.

Serpent in Python

class Serpent:
    """Serpent with a key of 1 to 32 bytes; encrypts and decrypts 16-byte blocks."""

    def __init__(self, key: bytes) -> None:
        if not 1 <= len(key) <= MAX_KEY_SIZE:
            elevate ValueError("Serpent keys are 1 to 32 bytes")
        # Pad to 256 bits: one 1 bit after the important thing, then zeros.
        padded = bytes(key) + (b"x01" if len(key) < MAX_KEY_SIZE else b"")
        padded = padded.ljust(MAX_KEY_SIZE, b"x00")
        # w[0..7] maintain the padded key; w[8..139] are the 132 prekey phrases.
        w = [int.from_bytes(padded[i : i + 4], "little") for i in vary(0, 32, 4)]
        for i in vary(8, 140):
            w.append(_rotl(w[i - 8] ^ w[i - 5] ^ w[i - 3] ^ w[i - 1] ^ PHI ^ (i - 8), 11))
        # Subkey i passes prekeys 4i..4i+3 by S-box (3 - i) mod 8.
        self._subkeys: checklist[Words] = [
            _apply_sbox(SBOX[(3 - i) % 8], w[8 + 4 * i : 12 + 4 * i]) for i in vary(33)
        ]

    def encrypt(self, block: bytes) -> bytes:
        x = _load(block)
        for r in vary(32):
            x = [a ^ k for a, k in zip(x, self._subkeys[r])]
            x = _apply_sbox(SBOX[r % 8], x)
            if r < 31:
                x = _linear_transform(x)
            else:  # the final spherical replaces LT with K32
                x = [a ^ k for a, k in zip(x, self._subkeys[32])]
        return _store(x)

    def decrypt(self, block: bytes) -> bytes:
        x = _load(block)
        for r in vary(31, -1, -1):
            if r == 31:
                x = [a ^ k for a, k in zip(x, self._subkeys[32])]
            else:
                x = _inverse_linear_transform(x)
            x = _apply_sbox(SBOX_INV[r % 8], x)
            x = [a ^ k for a, k in zip(x, self._subkeys[r])]
        return _store(x)

Python integers by no means overflow, so the rotation and linear-transformation helpers in the identical file masks each consequence with & 0xFFFFFFFF to remain inside 32 bits; forgetting one masks lets a price develop previous 32 bits and silently modifications the output. int.from_bytes(..., "little") and to_bytes(4, "little") deal with the byte order, and (3 - i) % 8 is already non-negative in Python. This model wants no third-party packages, and it’s greater than 200 instances slower than the compiled ones.

Serpent in C#

    /// <abstract>Expands a key of 1 to 32 bytes; shorter keys are padded because the specification requires.</abstract>
    public Serpent(ReadOnlySpan<byte> key)
    {
        if (key.Size is 0 or > MaxKeySize)
            throw new ArgumentException("Serpent keys are 1 to 32 bytes.", nameof(key));

        // Pad to 256 bits: one 1 bit after the important thing, then zeros.
        Span<byte> padded = stackalloc byte[MaxKeySize];
        padded.Clear();
        key.CopyTo(padded);
        if (key.Size < MaxKeySize)
            padded[key.Length] = 1;

        // w[0..7] maintain the padded key; w[8..139] are the 132 prekey phrases.
        Span<uint> w = stackalloc uint[140];
        for (int i = 0; i < 8; i++)
            w[i] = BinaryPrimitives.ReadUInt32LittleEndian(padded[(4 * i)..]);
        for (int i = 8; i < 140; i++)
            w[i] = BitOperations.RotateLeft(w[i - 8] ^ w[i - 5] ^ w[i - 3] ^ w[i - 1] ^ Phi ^ (uint)(i - 8), 11);

        // Subkey i passes prekeys 4i..4i+3 by S-box (3 - i) mod 8.
        for (int i = 0; i < 33; i++)
        {
            Span<uint> okay = _subkeys.AsSpan(4 * i, 4);
            w.Slice(8 + 4 * i, 4).CopyTo(okay);
            ApplySbox(Sbox[(35 - i) % 8], okay);        // 35 = 3 mod 8, by no means damaging
        }
        CryptographicOperations.ZeroMemory(padded);
        CryptographicOperations.ZeroMemory(System.Runtime.InteropServices.MemoryMarshal.AsBytes(w));
    }

    /// <abstract>Encrypts one 16-byte block from <paramref title="enter"/> into <paramref title="output"/>.</abstract>
    public void Encrypt(ReadOnlySpan<byte> enter, Span<byte> output)
    {
        Span<uint> x = stackalloc uint[4];
        Load(enter, x);
        for (int r = 0; r < 32; r++)
        {
            XorKey(x, r);
            ApplySbox(Sbox[r % 8], x);
            if (r < 31)
                LinearTransform(x);
            else
                XorKey(x, 32);                   // the final spherical replaces LT with K32
        }
        Retailer(x, output);
    }

The C# class works on spans, so a caller can encrypt in place inside a bigger buffer with out allocating. BinaryPrimitives.ReadUInt32LittleEndian fixes the byte order regardless of the machine, BitOperations.RotateLeft turns into a {hardware} rotate instruction the place the processor has one, the padded key and prekeys reside on the stack with stackalloc, and CryptographicOperations.ZeroMemory clears them in a approach the JIT won’t optimize away. Serpent implements IDisposable, and Dispose clears the subkeys.

4 Errors That Break a Serpent Implementation

A damaged cipher nonetheless produces output that appears random, and that’s what makes cipher bugs harmful. Every mistake under was checked towards the 1,043 check vectors, utilizing small packages within the C repository’s pitfalls listing or a intentionally altered copy of the code.

1. Testing Solely That Decryption Undoes Encryption

The commonest check of a cipher is the spherical journey: encrypt, decrypt, evaluate. It can not detect a bug that encryption and decryption share. round_trip_only.c flips one little bit of subkey K5, as a key-schedule bug would, and runs all of the vectors:

spherical journey succeeds:    1043 of 1043
matches the check vector: 0 of 1043

Each block decrypts completely, and never one ciphertext is Serpent. The identical occurred on a replica of the C code with a improper golden-ratio fixed, a improper prekey index and subkeys taken by the improper S-boxes: all 1,043 spherical journeys succeeded and no vector matched. A improper S-box entry is subtler. In C, which lists the inverse tables individually, it broke each spherical journey. In Python, which computes the inverses from the ahead tables, the identical typo decrypted all 1,043 blocks appropriately and matched no vector. Deriving the inverse removes one desk to mistype, and with it one method to discover the error. Identified-answer vectors from an unbiased supply are the one check that proves you carried out the cipher somewhat than a cipher.

2. Studying the Phrases within the Incorrect Byte Order

Loading every 32-bit phrase big-endian, the way in which a forged by ntohl() or a port to a big-endian machine would, offers a special and nonetheless reversible cipher. word_byte_order.c simulates it by swapping the bytes of every phrase across the appropriate code:

big-endian phrases match the check vectors: 0 of 1043

Write express little-endian hundreds and shops, as all 5 implementations do, somewhat than copying bytes right into a uint32_t with memcpy and hoping the machine is little-endian.

3. Testing Solely 256-Bit Keys

Padding is utilized solely to keys shorter than 256 bits. When the padding bit was faraway from a replica of the C code, the check suite reported:

1043 vectors: 0 with 128-bit keys, 0 with 192-bit, 458 with 256-bit; 585 failed

All 458 vectors with 256-bit keys nonetheless handed. A set that exams solely the longest key would have handed this bug, and with it each 128-bit key this system ever expanded would have been improper. Setting the padding bit to 0x80 as an alternative of 0x01, the opposite believable slip, gave the identical counts. Check each key measurement you settle for.

4. Encrypting a Message One Block at a Time

A block cipher encrypts precisely 16 bytes. Making use of it to every 16-byte block of an extended message independently known as ECB mode, and it leaks the message’s construction: equal plaintext blocks give equal ciphertext blocks. ecb_patterns.c encrypts a 48-byte message:

"PAY 100 DOLLARS " -> ed4988955ee313c89e8f9bda7616de2c
"PAY 100 DOLLARS " -> ed4988955ee313c89e8f9bda7616de2c
"PAY 999 DOLLARS " -> a3eb5fbd2bfad2e89e8f74b1a89bfd76

With out the important thing, an observer nonetheless learns that the primary two blocks are equivalent and the third is completely different. Actual methods wrap a block cipher in a mode equivalent to CTR or XTS, and add authentication so tampering is detected; the most secure alternative is an authenticated mode from a maintained library, lined under.

How Quick Is It?

How This Was Measured

Setting Worth
Date examined October 14, 2026
{Hardware} Cloud VM, 2 vCPUs, Intel Xeon @ 2.10 GHz
Working system Ubuntu 24.04.5 LTS, x86-64
C and C++ GCC 13.3.0, CMake Launch construct (-O3 -DNDEBUG); C++ constructed with -std=c++20
Java OpenJDK 21.0.12.1; one untimed warm-up run for the JIT
C# .NET 10.0.12, Launch construct; one untimed warm-up run
Python CPython 3.13.16
Reference library libgcrypt 1.10.3 (Ubuntu bundle), Serpent-256 in ECB mode
Enter A 16 MiB buffer (1 MiB for Python) full of the identical byte sample in each language; the 256-bit key 00 01 … 1f
What’s timed Encrypting the buffer one 16-byte block at a time; key setup and filling the buffer are excluded
Statistic Median of 5 runs
Verify Each implementation prints the XOR of all its ciphertext blocks, and all 5 agree; libgcrypt’s ciphertext is in contrast byte for byte with the C output

All figures come from one machine in a single session. No CPU pinning or frequency-scaling management was utilized, and no percentiles have been captured. The benchmark measures single-threaded encryption of 1 block after one other and says nothing about decryption, key setup, or a cipher mode.

Implementation Throughput
C (this web page) 3.88 MB/s
C++ (this web page) 4.52 MB/s
Java (this web page) 4.46 MB/s
C# (this web page) 4.55 MB/s
Python (this web page) 0.019 MB/s
libgcrypt 1.10.3 89.37 MB/s

The 4 compiled implementations land inside 20% of one another, so the language issues far lower than the algorithm’s type. Python is greater than 200 instances slower than they’re, at roughly 19 KB per second. libgcrypt is about 20 instances quicker than one of the best of them, for a purpose seen in its supply: it computes every S-box as a set sequence of logic operations on entire 32-bit phrases, so one sequence handles all 32 bit positions directly, as an alternative of the desk lookup per bit place that the code on this web page performs. That’s bitslicing used for velocity; the code right here makes use of the bitslice structure for readability. libgcrypt additionally has SSE2, AVX2 and ARM NEON variations that encrypt a number of blocks in parallel.

Is Serpent Nonetheless Safe?

As a cipher, sure. The very best printed key-recovery assaults attain 11 and 12 of Serpent’s 32 rounds. The 11-round assault by Nguyen, Wu and Wang (ACISP 2011) wants about 2¹¹⁶ recognized plaintexts, and the 12-round assault in the identical paper wants extra time nonetheless. No assault on the total 32 rounds comes near being sensible.

The code on this web page is a special matter, and it’s written for studying, not for shielding knowledge:

  • It has not been checked for timing aspect channels. Its S-box step indexes a 16-entry desk with bits that depend upon the important thing and the info. Such lookups can leak info by cache timing on some {hardware}, and nothing right here measures whether or not they do.
  • It’s only the block cipher. It has no mode, no authentication, no key derivation and no protected method to generate keys (by no means rand(); see random quantity technology in C and C++).. Every of these is the place actual methods often fail.
  • It’s gradual. At 4 MB/s, it’s a educating instrument.

For those who want Serpent itself, for compatibility with an present format, use a maintained library: libgcrypt, Botan and Bouncy Fortress all embrace it. For brand spanking new designs, the standard alternative is AES in an authenticated mode equivalent to AES-GCM, or ChaCha20-Poly1305. Each are normal, extensively reviewed and accessible in each main language, and most present x86 and lots of ARM processors have AES directions, which Serpent can not use.

Serpent vs AES

Facet Serpent AES (Rijndael)
Block measurement 128 bits 128 bits
Key sizes As much as 256 bits 128, 192 or 256 bits
Rounds 32 10, 12 or 14, by key measurement
S-boxes Eight 4-bit S-boxes One 8-bit S-box
{Hardware} help None in widespread CPUs AES directions on most present x86 and lots of ARM processors
Commonplace AES finalist, no FIPS normal FIPS 197
Licence Public area Royalty-free

Serpent’s further rounds purchase a bigger security margin at the price of velocity in software program. AES, standardized as FIPS 197 in November 2001, gained the competitors and the {hardware} help that got here with it, which is why AES is the default and Serpent is a alternative made for particular causes. Public-key methods equivalent to RSA work otherwise once more: they relaxation on quantity concept, equivalent to merchandise of huge prime numbers, whereas a block cipher like Serpent makes use of nothing greater than XOR, rotations and desk lookups.

Key Takeaways

  • Serpent is a 128-bit block cipher with keys as much as 256 bits and 32 rounds, an AES finalist that took second place, 59 votes to Rijndael’s 86, and is within the public area.
  • Every spherical is subkey XOR, S-box, linear transformation; the final spherical replaces the linear transformation with a closing subkey XOR.
  • The bitslice structure treats the block as 4 32-bit phrases, so the 32 S-box lookups of a spherical might be computed along with logic operations.
  • Two byte-order conventions exist. NESSIE order and the AES submission’s order are byte-reversals of one another; test which one a vector makes use of earlier than debugging.
  • Spherical-trip exams show nothing about correctness. A one-bit key-schedule bug round-tripped all 1,043 vectors and matched none of them.
  • Check each key measurement. A lacking padding bit handed each 256-bit vector and failed each shorter one.
  • Use a maintained library for actual knowledge. The implementations right here run at about 4 MB/s, libgcrypt at about 89, and not one of the code on this web page has been reviewed for aspect channels.

Ceaselessly Requested Questions

Conclusion

Serpent is an effective cipher to study from as a result of nothing in it’s hidden. 4 phrases, a 4-bit desk, ten strains of rotations and XORs, and a key schedule constructed on the golden ratio are the entire design, and 32 rounds of them produce a cipher that has outlasted each printed assault. The byte-order break up is a helpful lesson too: a cipher is outlined by its check vectors as a lot as by its specification.

The opposite lesson is about testing, and it applies effectively past cryptography. Code that agrees with itself proves nothing; code that agrees with another person’s printed solutions proves an ideal deal. Extra algorithms carried out and examined this manner are within the algorithms part.

Supply Code and Checks

All of the code on this web page, with its exams, is within the cryptography/serpent listing of every language’s MYCPLUS examples repository:

Construct and check the C and C++ variations:

cmake -S . -B construct
cmake --build construct
ctest --test-dir construct --output-on-failure

Java: javac -d construct src/*.java exams/*.java bench/*.java, then java -ea -cp construct SerpentTest exams/vectors/serpent_nessie.txt. Python: python3 -m unittest uncover -s exams -v. C#: dotnet construct Serpent.slnx -c Launch, then dotnet run --project exams/SerpentTests -c Launch --no-build.

What the construct checks. In each language: all 1,043 NESSIE vectors with 128-, 192- and 256-bit keys, encrypting and decrypting; rejection of 0- and 33-byte keys; and the demo printing precisely the output on this web page. C and C++ compile with GCC and Clang (C11 and C17; C++17 and C++20) and with MSVC /W4 /WX, warnings as errors, and run underneath AddressSanitizer and UndefinedBehaviorSanitizer. The C construct additionally checks that the three pitfall packages print the outputs proven above and, on Linux, compares 9,000 random keys and blocks with libgcrypt. Java runs on 17, 21 and 25, Python on 3.10 to three.14 with ruff and mypy --strict, each on Linux and Home windows, and C# on .NET 10 on Linux, Home windows and macOS. The builds don’t run the benchmarks, whose figures got here from one machine, and don’t check for timing aspect channels.

What I May Not Confirm

  • Facet channels. No implementation on this web page was examined for timing or cache leakage, on any {hardware}.
  • Different platforms. Every little thing was run on one x86-64 Linux machine. The MSVC, Home windows, macOS, Java 17 and 25, and Python 3.10–3.12 and three.14 jobs run solely within the repositories’ CI.
  • Which libgcrypt code path ran. The 89 MB/s determine is for the Ubuntu construct of libgcrypt 1.10.3 on this CPU. Its SIMD variations are described from libgcrypt’s supply code; I didn’t test which one the library chosen at run time.
  • The AES submission bundle itself. The submission-order test makes use of the 13 vectors in Bouncy Fortress’s TnepresTest, which states that they arrive from the submission; the submission’s personal information weren’t downloaded. The NESSIE vectors come from the copy distributed with Botan.
  • Cryptanalysis. The assault figures are taken from the printed paper and weren’t reproduced.
RELATED ARTICLES

LEAVE A REPLY

Please enter your comment!
Please enter your name here

Most Popular

Recent Comments