Inside Magma: How the Russian GOST R 34.12-2015 block cipher works

Date: 03/09/2025

This article introduces the Russian block cipher “Magma” (GOST 34.12-2015), an updated revision of the classic GOST 28147-89 with fixed S-boxes. It provides a detailed walkthrough of the design principles, key schedule, and encryption process, along with a clear C implementation. By the end, you’ll understand how Magma differs from Western block ciphers and how it can be used in practice.

The “Magma” algorithm is essentially an exact copy of the block cipher from the old GOST 28147-89, with one key difference. In the newer GOST 34.12-2015, the substitution boxes (S-boxes) for the nonlinear bijective transformation are explicitly defined and fixed, whereas in GOST 28147-89 they were not specified and implementers were free to choose their own S-box values.

In theory, if you define the S-box yourself and keep it secret, you can improve the cipher’s strength (it effectively increases the key size). However, as we can see, the authors of GOST 34.12–2015 chose to take that flexibility away from users.

As noted earlier, the Magma cipher uses a 64-bit block size and a 256-bit encryption key.

warning

When reading the GOST, keep in mind that in all 8‑byte arrays of the test vectors, byte 0 is at the end of the array and byte 7 is at the beginning. If you’ve read the articles about “Streebog” and “Kuznechik,” this quirk of our crypto standards should be familiar.

A bit of theory

In the algorithm described, the 64-bit block to be encrypted is split into two equal 32-bit halves—right and left. Then 32 rounds are performed using round keys derived from the original 256-bit encryption key.

Algorithm workflow for encryption
Algorithm workflow for encryption

In each round (except the thirty-second), a Feistel-based transformation is applied to the block’s right and left halves. First, the right half is added to the current round key modulo 2^32. The resulting 32-bit value is split into eight 4-bit chunks, and each chunk is substituted via an S-box (previously referred to as a nonlinear bijective transformation). The result is then rotated left by 11 bits and XORed with the left half. The resulting 32-bit value becomes the new right half, while the previous right half is moved to the left half.

Diagram of a single round
Diagram of a single round

In the final (thirty-second) round, the right half is processed as described above; the result is written into the left half of the original block, while the right half remains unchanged.

The round keys are derived from a 256-bit master key. The master key is split into eight 32-bit subkeys, which are applied in this order: three passes from the first through the eighth, and one pass from the eighth back to the first.

Round key derivation scheme
Round key derivation scheme

For decryption, the same sequence of iterations is used as for encryption, but the keys are applied in reverse order.

Decryption process diagram
Decryption process diagram

With a quick theoretical primer out of the way, let’s start coding.

Core functions of the standard

Since the algorithm operates on 32-bit blocks (as so-called binary vectors), let’s start by defining this block:

// Block size is 4 bytes (or 32 bits)
#define BLOCK_SIZE 4
...
// Define type vect as a 4-byte array
typedef uint8_t vect[BLOCK_SIZE];

Bitwise XOR of Two Binary Vectors

Each byte of the first vector is XORed with the corresponding byte of the second vector, and the result is written to a third (output) vector:

static void GOST_Magma_Add(const uint8_t *a, const uint8_t *b, uint8_t *c)
{
int i;
for (i = 0; i < BLOCK_SIZE; i++)
c[i] = a[i]^b[i];
}

Addition of Two Binary Vectors Modulo 2^32

This function is analogous to the “addition in the ring of residues modulo 2^n” used in the Streebog algorithm, except that here n is 32 rather than 512 as in the Streebog standard. The two input 4-byte vectors are treated as 32-bit integers and added; any overflow, if it occurs, is discarded:

static void GOST_Magma_Add_32(const uint8_t *a, const uint8_t *b, uint8_t *c)
{
int i;
unsigned int internal = 0;
for (i = 3; i >= 0; i--)
{
internal = a[i] + b[i] + (internal >> 8);
c[i] = internal & 0xff;
}
}

Nonlinear bijective transformation (T‑transformation)

Unlike the Streebog and Kuznyechik algorithms (there, this step is called the S-transformation/S-box), a different substitution table is used here.

static unsigned char Pi[8][16]=
{
{1,7,14,13,0,5,8,3,4,15,10,6,9,12,11,2},
{8,14,2,5,6,9,1,12,15,4,11,0,13,10,3,7},
{5,13,15,6,9,2,12,10,11,7,8,1,4,3,14,0},
{7,15,5,10,8,1,6,13,0,9,3,14,11,4,2,12},
{12,8,2,1,13,4,15,6,7,0,10,5,3,14,9,11},
{11,3,5,8,2,15,10,13,14,1,7,4,12,9,6,0},
{6,8,2,3,9,10,5,12,1,14,4,7,11,13,0,15},
{12,4,6,2,10,5,11,9,14,8,13,7,0,3,15,1}
};

Because in the standard’s text (by some obscure convention) byte 0 is listed last and the final byte first, the program will only work correctly if you write the table rows in reverse order—rather than as described in the standard.

The code for the T transformation function ends up looking like this:

static void GOST_Magma_T(const uint8_t *in_data, uint8_t *out_data)
{
uint8_t first_part_byte, sec_part_byte;
int i;
for (i = 0; i < 4; i++)
{
// Extract the first 4-bit part of the byte
first_part_byte = (in_data[i] & 0xf0) >> 4;
// Extract the second 4-bit part of the byte
sec_part_byte = (in_data[i] & 0x0f);
// Substitute according to the S-box table
first_part_byte = Pi[i * 2][first_part_byte];
sec_part_byte = Pi[i * 2 + 1][sec_part_byte];
// Merge both 4-bit parts back into a byte
out_data[i] = (first_part_byte << 4) | sec_part_byte;
}
}

Key schedule

As noted earlier, encryption and decryption require thirty-two 32-bit round keys derived from the original 256-bit key.

First, let’s decide where the retrieved key values will be stored:

vect iter_key[32]; // Encryption round keys

After that, you can proceed with the actual deployment of the keys:

void GOST_Magma_Expand_Key(const uint8_t *key)
{
// Generate eight 32-bit subkeys in order from first to eighth
memcpy(iter_key[0], key, 4);
memcpy(iter_key[1], key + 4, 4);
memcpy(iter_key[2], key + 8, 4);
memcpy(iter_key[3], key + 12, 4);
memcpy(iter_key[4], key + 16, 4);
memcpy(iter_key[5], key + 20, 4);
memcpy(iter_key[6], key + 24, 4);
memcpy(iter_key[7], key + 28, 4);
...
// Repeat the previous chunk of code two more times
...
// Generate eight 32-bit subkeys in order from eighth to first
memcpy(iter_key[24], key + 28, 4);
memcpy(iter_key[25], key + 24, 4);
memcpy(iter_key[26], key + 20, 4);
memcpy(iter_key[27], key + 16, 4);
memcpy(iter_key[28], key + 12, 4);
memcpy(iter_key[29], key + 8, 4);
memcpy(iter_key[30], key + 4, 4);
memcpy(iter_key[31], key, 4);
}
Result of GOST_Magma_Expand_Key using the example key from the standard
Result of GOST_Magma_Expand_Key using the example key from the standard

Sure, you could write all of this using loops, but here the function is implemented in a straightforward, straight-line way for clarity, and theoretically it should be faster in this form.

The g Transformation

The transformation includes adding the right half of the block to the round key modulo 2^32, applying a nonlinear bijective substitution (S-box), and rotating left by eleven bits:

static void GOST_Magma_g(const uint8_t *k, const uint8_t *a, uint8_t *out_data)
{
uint8_t internal[4];
uint32_t out_data_32;
// Add modulo 2^32 the right half of the block with the round key
GOST_Magma_Add_32(a, k, internal);
// Apply a nonlinear bijective transformation to the result
GOST_Magma_T(internal, internal);
// Convert the 4-byte vector into a single 32-bit number
out_data_32 = internal[0];
out_data_32 = (out_data_32 << 8) + internal[1];
out_data_32 = (out_data_32 << 8) + internal[2];
out_data_32 = (out_data_32 << 8) + internal[3];
// Rotate left by 11 bits
out_data_32 = (out_data_32 << 11)|(out_data_32 >> 21);
// Convert the 32-bit rotated result back into a 4-byte vector
out_data[3] = out_data_32;
out_data[2] = out_data_32 >> 8;
out_data[1] = out_data_32 >> 16;
out_data[0] = out_data_32 >> 24;
}

G-Transformation

This transformation represents a single round of the encryption or decryption cycle (from the first through the thirty-first). It includes applying the g transformation, XORing (addition modulo 2) the result of g with the right half of the block, and swapping the contents of the left and right halves of the block.

static void GOST_Magma_G(const uint8_t *k, const uint8_t *a, uint8_t *out_data)
{
uint8_t a_0[4]; // Right half of the block
uint8_t a_1[4]; // Left half of the block
uint8_t G[4];
int i;
// Split the 64-bit input block into two halves
for(i = 0; i < 4; i++)
{
a_0[i] = a[4 + i];
a_1[i] = a[i];
}
// Apply the g transformation
GOST_Magma_g(k, a_0, G);
// XOR the g transformation result with the left half of the block
GOST_Magma_Add(a_1, G, G);
for(i = 0; i < 4; i++)
{
// Write the right half into the left half
a_1[i] = a_0[i];
// Write the result of GOST_Magma_Add into the right half
a_0[i] = G[i];
}
// Combine the right and left halves back into a single block
for(i = 0; i < 4; i++)
{
out_data[i] = a_1[i];
out_data[4 + i] = a_0[i];
}
}

Final G Transformation

This is the final (thirty-second) round of encryption or decryption. Unlike the simple G transformation, it does not swap the values between the right and left halves of the input block:

static void GOST_Magma_G_Fin(const uint8_t *k, const uint8_t *a, uint8_t *out_data)
{
uint8_t a_0[4]; // Right half of the block
uint8_t a_1[4]; // Left half of the block
uint8_t G[4];
int i;
// Split the 64-bit input block into two parts
for(i = 0; i < 4; i++)
{
a_0[i] = a[4 + i];
a_1[i] = a[i];
}
// Perform the g transformation
GOST_Magma_g(k, a_0, G);
// XOR the result of the g transformation with the left half of the block
GOST_Magma_Add(a_1, G, G);
// Write the result of GOST_Magma_Add into the left half of the block
for(i = 0; i < 4; i++)
a_1[i] = G[i];
// Combine the right and left halves back into a single block
for(i = 0; i < 4; i++)
{
out_data[i] = a_1[i];
out_data[4 + i] = a_0[i];
}
}

Encrypting

As noted earlier, encryption runs for 32 iterations: rounds 1 through 31 use the G transformation, and round 32 uses the final G transformation:

void GOST_Magma_Encript(const uint8_t *blk, uint8_t *out_blk)
{
int i;
// First G transformation
GOST_Magma_G(iter_key[0], blk, out_blk);
// Subsequent G transformations (from the 2nd to the 31st)
for(i = 1; i < 31; i++)
GOST_Magma_G(iter_key[i], out_blk, out_blk);
// Final (32nd) G transformation
GOST_Magma_G_Fin(iter_key[31], out_blk, out_blk);
}

Before calling this function, make sure to invoke GOST_Magma_ExpandKey to populate the round key array with the required values.

Decryption

Decryption is performed similarly to encryption, using the round keys in reverse order:

void GOST_Magma_Decript(const uint8_t *blk, uint8_t *out_blk)
{
int i;
// First G transformation using
// the thirty-second iteration key
GOST_Magma_G(iter_key[31], blk, out_blk);
// Subsequent (from the second to the thirty-first) G transformations
// (iteration keys go in reverse order)
for(i = 30; i > 0; i--)
GOST_Magma_G(iter_key[i], out_blk, out_blk);
// Final (thirty-second) G transformation
// using the first iteration key
GOST_Magma_G_Fin(iter_key[0], out_blk, out_blk);
}

Also, don’t forget about GOST_Magma_ExpandKey, since decryption uses the same round keys as encryption.

Conclusion

The Magma cipher from GOST R 34.12-2015 is much simpler to implement than the Kuznyechik (Grasshopper) cipher from the same standard, and it is currently used far more widely (including in the GOST 28147-89 variant with S-boxes defined by implementers themselves or taken from RFC 4357).

You can now encrypt both 128-bit blocks (using Kuznyechik, aka Grasshopper) and 64-bit blocks (using Magma). Bulk-encryption procedures and modes of operation for these two ciphers—namely ECB (simple substitution), CTR (gamma), OFB (gamma with output feedback), CBC (simple substitution with chaining), CFB (gamma with ciphertext feedback), and MAC generation—are specified in a separate standard: GOST 34.13–2015 “Information technology. Cryptographic data security. Modes of operation of block ciphers.” More on this in upcoming articles.

Related posts:
2023.02.21 — Pivoting District: GRE Pivoting over network equipment

Too bad, security admins often don't pay due attention to network equipment, which enables malefactors to hack such devices and gain control over them. What…

Full article →
2023.02.13 — First Contact: Attacks on Google Pay, Samsung Pay, and Apple Pay

Electronic wallets, such as Google Pay, Samsung Pay, and Apple Pay, are considered the most advanced and secure payment tools. However, these systems are also…

Full article →
2022.01.12 — First contact. Attacks against contactless cards

Contactless payment cards are very convenient: you just tap the terminal with your card, and a few seconds later, your phone rings indicating that…

Full article →
2022.02.15 — Reverse shell of 237 bytes. How to reduce the executable file using Linux hacks

Once I was asked: is it possible to write a reverse shell some 200 bytes in size? This shell should perform the following functions: change its name…

Full article →
2022.01.13 — Bug in Laravel. Disassembling an exploit that allows RCE in a popular PHP framework

Bad news: the Ignition library shipped with the Laravel PHP web framework contains a vulnerability. The bug enables unauthorized users to execute arbitrary code. This article examines…

Full article →
2022.06.01 — F#ck AMSI! How to bypass Antimalware Scan Interface and infect Windows

Is the phrase "This script contains malicious content and has been blocked by your antivirus software" familiar to you? It's generated by Antimalware Scan Interface…

Full article →
2023.04.20 — Sad Guard. Identifying and exploiting vulnerability in AdGuard driver for Windows

Last year, I discovered a binary bug in the AdGuard driver. Its ID in the National Vulnerability Database is CVE-2022-45770. I was disassembling the ad blocker and found…

Full article →
2023.07.07 — VERY bad flash drive. BadUSB attack in detail

BadUSB attacks are efficient and deadly. This article explains how to deliver such an attack, describes in detail the preparation of a malicious flash drive required for it,…

Full article →
2022.02.09 — F#ck da Antivirus! How to bypass antiviruses during pentest

Antiviruses are extremely useful tools - but not in situations when you need to remain unnoticed on an attacked network. Today, I will explain how…

Full article →
2022.06.03 — Playful Xamarin. Researching and hacking a C# mobile app

Java or Kotlin are not the only languages you can use to create apps for Android. C# programmers can develop mobile apps using the Xamarin open-source…

Full article →