The GOST 28147-89 Algorithm
GOST 28147-89 was the Soviet Union's answer to DES. Learn how its 32-round Feistel design works, why the standard left the S-boxes undefined, and how the modern Magma cipher fixed that.
Interactive GOST 28147-89 Encryption
🔐 GOST 28147-89 Encryption
The GOST 28147-89 Algorithm
Introduction
GOST 28147-89 is the Soviet and Russian government block cipher. It encrypts 64-bit blocks with a 256-bit key through 32 rounds, and it was the Soviet alternative to the American DES. The two are very similar in structure, although GOST is simpler: it has no initial permutation, no key schedule, and only one repeated round function.
It has one striking oddity. The original standard did not say what the S-boxes are. They are a parameter, so two systems that both claim to use GOST 28147-89 can fail to talk to each other. The 2015 revision, which names the cipher Magma, finally fixed the S-boxes.
Table of Contents
- History
- How GOST Works
- The S-Box Parameter
- Key Schedule
- Byte Order
- A Worked Example
- Python Implementation
- Limitations
- Security Status
- FAQ
- References
History
GOST 28147-89 was developed in the 1970s inside the Soviet KGB. The standard was marked Top Secret and then downgraded to Secret in 1990. It was declassified and released to the public in 1994, after the end of the Soviet Union.
For years the cipher had no name beyond its standard number. An English description appeared as RFC 5830 in 2010, and RFC 4357 listed several S-box sets that products actually used. In 2015 the standard GOST R 34.12-2015 gave the 64-bit cipher the name Magma and fixed its S-boxes. It also added a second cipher, Kuznyechik, which has a 128-bit block. RFC 8891 describes Magma in English.
How GOST Works
The cipher splits the 64-bit block into two 32-bit halves. Throughout this guide, N1 is the half that goes through the round function and N2 is the half it is XORed into. Each round does this:
- Add the subkey. Compute
N1 + Kmodulo 2³². - Substitute. Cut the 32-bit sum into eight 4-bit pieces and replace each one using its own S-box.
- Rotate. Rotate the 32-bit result left by 11 bits.
- XOR and swap. XOR the result into
N2, then swap the halves.
The last round skips the swap. Decryption is the very same procedure with the subkeys in reverse order, like DES, so a single routine does both jobs.
Interactive Visualizer
The visualizer above runs this exact algorithm. Pick Magma or one of two classic GOST 28147-89 S-box sets, then encrypt a block. Each row of the log shows the two halves after a round, and the note shows the round function output and the subkey that was used.
The S-Box Parameter
GOST uses eight small S-boxes, one for each 4-bit piece of the 32-bit word. Each maps a 4-bit input to a 4-bit output, so it is a table of 16 entries. The standard itself does not define any S-boxes. RFC 5830 puts it plainly: the filling of the S-boxes is described as a long-term key element common to a whole computer network.
In practice, implementations picked a published set. RFC 4357 lists several, including a “test” set and a series of “CryptoPro” sets. Magma uses a single fixed set called id-tc26-gost-28147-param-Z. The choice matters enormously. The same key and the same plaintext produce a completely different ciphertext under every different S-box set, as the worked example below shows.
Each S-box is applied to its own nibble of the word. The first S-box handles the lowest four bits, and the eighth handles the highest four.
Key Schedule
GOST has no real key schedule. The 256-bit key is cut into eight 32-bit words, K1 to K8, and those words are used directly as subkeys in a fixed order:
rounds 1-8: K1 K2 K3 K4 K5 K6 K7 K8
rounds 9-16: K1 K2 K3 K4 K5 K6 K7 K8
rounds 17-24: K1 K2 K3 K4 K5 K6 K7 K8
rounds 25-32: K8 K7 K6 K5 K4 K3 K2 K1
That is simple and fast, and it needs no setup time at all. It also means the cipher’s structure repeats in a regular way, and attacks on the full cipher have made use of that regularity.
Byte Order
GOST 28147-89 and Magma disagree about byte order, and mixing them up is a classic source of bugs. GOST 28147-89 reads the key and the block as little-endian words. Magma reads the key and the block as big-endian numbers.
Apart from the S-boxes, that is the only difference. Magma is GOST 28147-89 with the id-tc26-gost-28147-param-Z S-boxes and the endianness change. The Python code below includes a flag for the byte order, and the verification section confirms the relationship on random inputs.
A Worked Example
This is the test vector from RFC 8891, which describes Magma. The key is ffeeddccbbaa99887766554433221100f0f1f2f3f4f5f6f7f8f9fafbfcfdfeff and the plaintext is fedcba9876543210.
Key schedule. The key words, read as big-endian, are K1 = ffeeddcc, K2 = bbaa9988, and so on up to K8 = fcfdfeff. Rounds 25 to 32 use them backward, so round 25 uses K8 = fcfdfeff and round 32 uses K1 = ffeeddcc.
Starting halves. The block splits into a1 = fedcba98 and a0 = 76543210, where a0 is the half that goes into the round function.
Round 1. Add the first subkey: a0 + K1 = 76430fdc modulo 2³². The eight S-boxes turn that into 319ac0d0. Rotating left by 11 bits gives d606818c, and XORing with a1 gives 28da3b14. After the swap the state is:
(a1, a0) = (76543210, 28da3b14)
Later rounds. After round 2 the state is (28da3b14, b14337a5). After round 8 it is (37d97f25, 46324615), after round 24 it is (8025c0a5, b0d66514), and after round 31 it is (239a4577, c2d8ca3d).
Final round. Round 32 uses K1 again but skips the swap, and the ciphertext is:
4ee901e5c2d8ca3d
RFC 8891 prints every one of these intermediate states. The Python code below matches all 31 of them.
The S-box effect. Now give the same key and plaintext to GOST 28147-89 instead, reading them as little-endian. With the Magma S-boxes the ciphertext is 8fc6feb891514c37. With the test S-boxes it is f9393352f83fe2ed, and with the CryptoPro S-boxes it is a976f43c73d02f9a. Even the byte order alone changes the result: Magma’s S-boxes under big-endian reading give 4ee901e5c2d8ca3d, and under little-endian reading give 8fc6feb891514c37.
Python Implementation
This is a complete GOST: all 32 rounds, three S-box sets, and both byte orders. Every S-box value was copied out of an RFC by a script, and each table was cross-checked against a second RFC where one exists.
# gost.py
#
# GOST 28147-89, the Soviet block cipher, and its modern descendant Magma
# (GOST R 34.12-2015). A 64-bit block, a 256-bit key, and 32 Feistel rounds.
# Each round adds a subkey, runs the result through eight 4-bit S-boxes,
# rotates left by 11, and XORs it into the other half.
#
# The old standard never fixed the S-boxes, so they are a parameter here.
# Magma fixes them, and also reads keys and blocks as big-endian numbers,
# where GOST 28147-89 reads them as little-endian.
MASK = 0xFFFFFFFF
# Eight S-boxes per set, S[0] applies to the lowest nibble. Values are
# copied from RFC 8891 / RFC 7836 (Magma) and RFC 4357 (the other two).
SBOXES = {
"magma": [
[12, 4, 6, 2, 10, 5, 11, 9, 14, 8, 13, 7, 0, 3, 15, 1],
[ 6, 8, 2, 3, 9, 10, 5, 12, 1, 14, 4, 7, 11, 13, 0, 15],
[11, 3, 5, 8, 2, 15, 10, 13, 14, 1, 7, 4, 12, 9, 6, 0],
[12, 8, 2, 1, 13, 4, 15, 6, 7, 0, 10, 5, 3, 14, 9, 11],
[ 7, 15, 5, 10, 8, 1, 6, 13, 0, 9, 3, 14, 11, 4, 2, 12],
[ 5, 13, 15, 6, 9, 2, 12, 10, 11, 7, 8, 1, 4, 3, 14, 0],
[ 8, 14, 2, 5, 6, 9, 1, 12, 15, 4, 11, 0, 13, 10, 3, 7],
[ 1, 7, 14, 13, 0, 5, 8, 3, 4, 15, 10, 6, 9, 12, 11, 2],
],
"test": [
[ 4, 10, 9, 2, 13, 8, 0, 14, 6, 11, 1, 12, 7, 15, 5, 3],
[14, 11, 4, 12, 6, 13, 15, 10, 2, 3, 8, 1, 0, 7, 5, 9],
[ 5, 8, 1, 13, 10, 3, 4, 2, 14, 15, 12, 7, 6, 0, 9, 11],
[ 7, 13, 10, 1, 0, 8, 9, 15, 14, 4, 6, 12, 11, 2, 5, 3],
[ 6, 12, 7, 1, 5, 15, 13, 8, 4, 10, 9, 14, 0, 3, 11, 2],
[ 4, 11, 10, 0, 7, 2, 1, 13, 3, 6, 8, 5, 9, 12, 15, 14],
[13, 11, 4, 1, 3, 15, 5, 9, 0, 10, 14, 7, 6, 8, 2, 12],
[ 1, 15, 13, 0, 5, 7, 10, 4, 9, 2, 3, 14, 6, 11, 8, 12],
],
"cryptopro": [
[10, 4, 5, 6, 8, 1, 3, 7, 13, 12, 14, 0, 9, 2, 11, 15],
[ 5, 15, 4, 0, 2, 13, 11, 9, 1, 7, 6, 3, 12, 14, 10, 8],
[ 7, 15, 12, 14, 9, 4, 1, 0, 3, 11, 5, 2, 6, 10, 8, 13],
[ 4, 10, 7, 12, 0, 15, 2, 8, 14, 1, 6, 5, 13, 11, 9, 3],
[ 7, 6, 4, 11, 9, 12, 2, 10, 1, 8, 0, 14, 15, 13, 3, 5],
[ 7, 6, 2, 4, 13, 9, 15, 0, 10, 1, 5, 11, 8, 14, 12, 3],
[13, 14, 4, 1, 7, 0, 5, 10, 3, 12, 8, 15, 6, 2, 9, 11],
[ 1, 3, 10, 9, 5, 11, 4, 15, 8, 6, 7, 14, 13, 0, 2, 12],
],
}
def rol11(x):
return ((x << 11) | (x >> 21)) & MASK
def f(x, subkey, sboxes):
"""Add the subkey, substitute eight nibbles, rotate left by 11."""
v = (x + subkey) & MASK
out = 0
for i in range(8):
out |= sboxes[i][(v >> (4 * i)) & 15] << (4 * i)
return rol11(out)
def round_keys(key, order):
words = [int.from_bytes(key[4 * i:4 * i + 4], order) for i in range(8)]
return words * 3 + words[::-1]
def crypt_block(keys, block, sboxes, magma):
order = "big" if magma else "little"
first, second = (int.from_bytes(block[i:i + 4], order) for i in (0, 4))
# a0 is the word that goes through f; a1 is the word it is XORed into.
a0, a1 = (second, first) if magma else (first, second)
for i in range(31):
a0, a1 = f(a0, keys[i], sboxes) ^ a1, a0
a1 = f(a0, keys[31], sboxes) ^ a1 # last round: no swap
pair = (a1, a0) if magma else (a0, a1)
return b"".join(w.to_bytes(4, order) for w in pair)
def encrypt_block(key, block, sbox="magma", magma=False):
return crypt_block(round_keys(key, "big" if magma else "little"), block, SBOXES[sbox], magma)
def decrypt_block(key, block, sbox="magma", magma=False):
keys = round_keys(key, "big" if magma else "little")[::-1]
return crypt_block(keys, block, SBOXES[sbox], magma)
if __name__ == "__main__":
key = bytes.fromhex("ffeeddccbbaa99887766554433221100f0f1f2f3f4f5f6f7f8f9fafbfcfdfeff")
plaintext = bytes.fromhex("fedcba9876543210")
ciphertext = encrypt_block(key, plaintext, "magma", magma=True)
recovered = decrypt_block(key, ciphertext, "magma", magma=True)
print(f"Key: {key.hex()}")
print(f"Plaintext: {plaintext.hex()}")
print(f"Magma: {ciphertext.hex()}")
print(f"Recovered: {recovered.hex()}")
old = encrypt_block(key, plaintext, "cryptopro")
print(f"GOST 28147-89 with CryptoPro S-boxes: {old.hex()}")
Running it produces this output:
Key: ffeeddccbbaa99887766554433221100f0f1f2f3f4f5f6f7f8f9fafbfcfdfeff
Plaintext: fedcba9876543210
Magma: 4ee901e5c2d8ca3d
Recovered: fedcba9876543210
GOST 28147-89 with CryptoPro S-boxes: a976f43c73d02f9a
I checked this code against three sources before writing it up. For Magma it reproduces every item in RFC 8891’s examples: the t and g transformations, all 32 round keys, all 31 intermediate states, the ciphertext, and decryption. For the original byte order it matches 28 test vectors from the Botan library, covering two different S-box sets. It also confirms that Magma is exactly GOST 28147-89 with the Z S-boxes and the endianness change, on 200 random key and block pairs.
For Fun: The Same Cipher in 9 Lines
This is the same spirit as the compact bonus sections elsewhere on this site. It is not for learning the algorithm from. This version squeezes the 102-line implementation above into 9 lines. Line 1 holds the three S-box sets, and the round function, key schedule, and block routines are written as tight one-liners. It needs no imports and no other files.
MASK=0xFFFFFFFF; SBOXES={"magma":[[12,4,6,2,10,5,11,9,14,8,13,7,0,3,15,1],[6,8,2,3,9,10,5,12,1,14,4,7,11,13,0,15],[11,3,5,8,2,15,10,13,14,1,7,4,12,9,6,0],[12,8,2,1,13,4,15,6,7,0,10,5,3,14,9,11],[7,15,5,10,8,1,6,13,0,9,3,14,11,4,2,12],[5,13,15,6,9,2,12,10,11,7,8,1,4,3,14,0],[8,14,2,5,6,9,1,12,15,4,11,0,13,10,3,7],[1,7,14,13,0,5,8,3,4,15,10,6,9,12,11,2]],"test":[[4,10,9,2,13,8,0,14,6,11,1,12,7,15,5,3],[14,11,4,12,6,13,15,10,2,3,8,1,0,7,5,9],[5,8,1,13,10,3,4,2,14,15,12,7,6,0,9,11],[7,13,10,1,0,8,9,15,14,4,6,12,11,2,5,3],[6,12,7,1,5,15,13,8,4,10,9,14,0,3,11,2],[4,11,10,0,7,2,1,13,3,6,8,5,9,12,15,14],[13,11,4,1,3,15,5,9,0,10,14,7,6,8,2,12],[1,15,13,0,5,7,10,4,9,2,3,14,6,11,8,12]],"cryptopro":[[10,4,5,6,8,1,3,7,13,12,14,0,9,2,11,15],[5,15,4,0,2,13,11,9,1,7,6,3,12,14,10,8],[7,15,12,14,9,4,1,0,3,11,5,2,6,10,8,13],[4,10,7,12,0,15,2,8,14,1,6,5,13,11,9,3],[7,6,4,11,9,12,2,10,1,8,0,14,15,13,3,5],[7,6,2,4,13,9,15,0,10,1,5,11,8,14,12,3],[13,14,4,1,7,0,5,10,3,12,8,15,6,2,9,11],[1,3,10,9,5,11,4,15,8,6,7,14,13,0,2,12]]}
def rol11(x): return ((x<<11)|(x>>21))&MASK
def f(x,subkey,sboxes): v=(x+subkey)&MASK; s=sum(sboxes[i][(v>>(4*i))&15]<<(4*i) for i in range(8)); return ((s<<11)|(s>>21))&MASK
def round_keys(key,order): words=[int.from_bytes(key[4*i:4*i+4],order) for i in range(8)]; return words*3+words[::-1]
def crypt_block(keys,block,sboxes,magma):
order="big" if magma else "little"; first,second=(int.from_bytes(block[i:i+4],order) for i in (0,4)); a0,a1=(second,first) if magma else (first,second)
for i in range(31): a0,a1=f(a0,keys[i],sboxes)^a1,a0
a1=f(a0,keys[31],sboxes)^a1; pair=(a1,a0) if magma else (a0,a1); return b"".join(w.to_bytes(4,order) for w in pair)
encrypt_block=lambda key,block,sbox="magma",magma=False: crypt_block(round_keys(key,"big" if magma else "little"),block,SBOXES[sbox],magma); decrypt_block=lambda key,block,sbox="magma",magma=False: crypt_block(round_keys(key,"big" if magma else "little")[::-1],block,SBOXES[sbox],magma)
if __name__=="__main__": key=bytes.fromhex("ffeeddccbbaa99887766554433221100f0f1f2f3f4f5f6f7f8f9fafbfcfdfeff"); plaintext=bytes.fromhex("fedcba9876543210"); ciphertext=encrypt_block(key,plaintext,"magma",magma=True); recovered=decrypt_block(key,ciphertext,"magma",magma=True); print(f"Key: {key.hex()}"); print(f"Plaintext: {plaintext.hex()}"); print(f"Magma: {ciphertext.hex()}"); print(f"Recovered: {recovered.hex()}"); old=encrypt_block(key,plaintext,"cryptopro"); print(f"GOST 28147-89 with CryptoPro S-boxes: {old.hex()}")
Running it prints the same five lines as the readable version, including the RFC 8891 Magma ciphertext:
Key: ffeeddccbbaa99887766554433221100f0f1f2f3f4f5f6f7f8f9fafbfcfdfeff
Plaintext: fedcba9876543210
Magma: 4ee901e5c2d8ca3d
Recovered: fedcba9876543210
GOST 28147-89 with CryptoPro S-boxes: a976f43c73d02f9a
I checked it against the readable code on 600 random keys and blocks, 100 for each combination of the three S-box sets and the two byte orders. Encryption matched every time, and decryption recovered every block. The S-box tables are identical to the readable ones.
Limitations
This is a faithful teaching version of the cipher, not a production one:
- Single 64-bit block only. There is no mode of operation for longer messages and no padding for partial blocks.
- The S-box choice is yours. The code offers three sets, but a real deployment must agree with its peers on one set and on the byte order.
- Table lookups depend on secret data. The S-box indexes come from secret values, so real software must consider cache-timing leaks that this plain code does not address.
- A 64-bit block is a real limit. See the Security Status section below.
Security Status
Since 2011, several attacks on the full 32-round cipher have been published, starting with work by Isobe, whose attack needed about 2²²⁴ time. Dinur, Dunkelman, and Shamir reported attacks with about 2²²⁴ time and 2³² data, and about 2¹⁹² time with 2⁶⁴ data. These attacks cut the nominal 2²⁵⁶ work of the 256-bit key, which was a notable result for a cipher considered strong for two decades. They are still far beyond practical computation, so GOST is not broken in practice.
The more immediate weakness is the 64-bit block. Like DES, 3DES, Blowfish, and IDEA, GOST is unsuitable for encrypting very large volumes of data under one key, for the birthday-bound reasons covered in the 3DES guide. Russia’s newer standard adds Kuznyechik, with a 128-bit block, for that reason.
FAQ
Why does GOST not specify its S-boxes?
The original standard treated them as a secret, long-term key element shared across a network. Different organizations could use different sets. The 2015 revision removed the ambiguity for Magma by fixing one set.
What is the difference between GOST 28147-89 and Magma?
Magma is the 2015 name for the cipher, with the S-boxes fixed to one specific set and with big-endian reading of keys and blocks. Otherwise the 32-round structure is identical.
Is GOST similar to DES?
Yes, in outline, because both are Feistel ciphers with a 64-bit block that use S-boxes. GOST has 32 rounds, a 256-bit key, eight 4-bit S-boxes, and no key schedule, while DES has 16 rounds, a 56-bit key, and a complex one.
Is GOST 28147-89 still secure?
Nothing practical breaks it. Attacks on the full cipher exist but cost far more than any computer can do. Its small 64-bit block limits how much data you should encrypt under one key.
Why rotate left by 11?
A rotation moves each S-box’s output bits into other 4-bit groups, so the next round mixes them together. The amount 11 also shares no factor with 32, so repeated rotations by 11 reach every bit position.
References
-
Dolmatov, V., Ed. “GOST 28147-89: Encryption, Decryption, and Message Authentication Code (MAC) Algorithms.” RFC 5830, 2010. Available at: https://www.rfc-editor.org/rfc/rfc5830
-
Popov, V., Kurepkin, I., and Leontiev, S. “Additional Cryptographic Algorithms for Use with GOST 28147-89, GOST R 34.10-94, GOST R 34.10-2001, and GOST R 34.11-94 Algorithms.” RFC 4357, 2006.
-
Dolmatov, V. and Baryshkov, D. “GOST R 34.12-2015: Block Cipher ‘Magma’.” RFC 8891, 2020. Available at: https://www.rfc-editor.org/rfc/rfc8891
-
Dinur, I., Dunkelman, O., and Shamir, A. “Improved Attacks on Full GOST.” Fast Software Encryption (FSE) 2012.
-
Wikipedia. “GOST (block cipher).” Available at: https://en.wikipedia.org/wiki/GOST_(block_cipher)