The RC5 and RC6 Algorithms
RC5 and RC6 are built from just three operations: addition, XOR, and rotations whose amounts come from the data itself. Learn how Ron Rivest's flexible RC5 led to RC6, an AES finalist.
Interactive RC5 and RC6 Encryption
🔐 RC5 and RC6 Encryption
The RC5 and RC6 Algorithms
Introduction
RC5 is a block cipher that Ron Rivest designed in 1994, and RC6 is its descendant, built in 1998 for the AES competition. Both are astonishingly short. RC5 fits in a handful of lines, and it uses only three operations: addition, XOR, and rotation.
The twist is the rotation. In most ciphers, the number of bits to rotate is fixed in the design. In RC5 and RC6 it is taken from the data being encrypted, so every block is scrambled in a slightly different way. Rivest wanted that idea studied, and these two ciphers are the result.
Table of Contents
- History
- How RC5 Works
- Data-Dependent Rotations
- Key Schedule
- How RC6 Works
- A Worked Example
- Python Implementation
- Limitations
- Security Status
- FAQ
- References
History
Rivest published RC5 in 1994 and described it as a way to prompt the study of data-dependent rotations as a cryptographic tool. He made the cipher fully parameterized. RC5 is written RC5-w/r/b, where w is the word size in bits, r is the number of rounds, and b is the key length in bytes.
The block holds two words, so the block size is 2w. Word sizes of 16, 32, and 64 bits give blocks of 32, 64, and 128 bits. Rounds can range from 0 to 255 and keys from 0 to 255 bytes. The recommended choice for 64-bit blocks was RC5-32/12/16: 32-bit words, 12 rounds, and a 128-bit key.
RC5 also became famous for a different reason. RSA Security offered prizes for breaking RC5 ciphertexts, and volunteers in the distributed.net project brute-forced the 56-bit and 64-bit challenges. A search for the 72-bit key began in 2002.
RC6 came from Ron Rivest, Matt Robshaw, Ray Sidney, and Yiqun Lisa Yin. It was one of the five AES finalists in 1998 and was not selected. RC6 has a 128-bit block, four words, and 20 rounds, and it is best seen as two RC5 processes woven together with a multiplication added. RSA held patents on both ciphers, and they have now expired.
How RC5 Works
RC5-32/12/16 treats a 64-bit block as two 32-bit words, A and B, read as little-endian. The round keys form a table S of 2r + 2 words. Encryption is only this:
A = A + S[0]
B = B + S[1]
for i = 1 to r:
A = ((A ^ B) <<< B) + S[2i]
B = ((B ^ A) <<< A) + S[2i+1]
All additions are modulo 2³². Each round updates A using B, then updates B using the new A. Decryption runs the same steps backward, subtracting where encryption added and rotating right where encryption rotated left.
Interactive Visualizer
The visualizer above runs this exact algorithm. Choose RC5 with 8, 12, or 16 rounds, or one of the three RC6 key sizes. Each row of the log records the state after a round, and the note shows the rotation amounts that the data itself chose for that round.
Data-Dependent Rotations
Look at the round again: A = ((A ^ B) <<< B) + S[2i]. The expression <<< B means “rotate left by the value of B.” Since rotating a 32-bit word by 32 does nothing, only the low five bits of B matter, so the amount is always between 0 and 31.
This is what sets RC5 apart. Differential and linear attacks work by following how bit patterns move through a cipher, and they depend on predicting which bits go where. When the rotation distance changes with the data, that prediction is much harder. The cipher also needs no S-boxes at all, so it is compact and runs the same on almost any processor that has a rotate instruction.
Key Schedule
RC5 and RC6 use the same key expansion, and it works on any key length. The key bytes are first packed into a list L of little-endian 32-bit words. The table S is then filled with a fixed pattern:
S[0] = P32 = 0xB7E15163
S[i] = S[i-1] + Q32, Q32 = 0x9E3779B9
These two constants are “nothing up my sleeve” numbers. P32 comes from the fractional part of e, the base of natural logarithms, and Q32 from the fractional part of the golden ratio. Each is scaled to 32 bits and rounded to the nearest odd integer.
The expansion then mixes the key into the table. It runs 3 × max(t, c) iterations, where t is the table size and c is the number of key words. Each iteration updates one word of S and one word of L:
A = S[i] = (S[i] + A + B) <<< 3
B = L[j] = (L[j] + A + B) <<< (A + B)
Three passes over the longer of the two arrays make sure every key bit affects every table word. RC5 needs t = 2r + 2 words and RC6 needs t = 2r + 4.
How RC6 Works
RC6 handles a 128-bit block as four words, A, B, C, and D. It runs 20 rounds, using a table of 44 words. Each round does this:
t = (B * (2B + 1)) <<< 5
u = (D * (2D + 1)) <<< 5
A = ((A ^ t) <<< u) + S[2i]
C = ((C ^ u) <<< t) + S[2i+1]
(A, B, C, D) = (B, C, D, A)
The new ingredient is the function x(2x + 1). RC5 rotates by only the low five bits of a word, so the rotation depends on just those five bits. Multiplication makes every bit of the input affect the low-order bits of the output. That means the rotation amount in RC6 depends on all 32 bits of B or D, which is the main fix over RC5’s rotations. The function x(2x + 1) is also a permutation of the word values, so it loses no information. I confirmed that for 16-bit words by trying every input.
At the end of the rounds, RC6 adds the last two table words to A and C. RC6 uses a 128-bit block like AES, and it accepts the same key sizes of 128, 192, and 256 bits.
A Worked Example
RC5-32/12/16. The key is 01020304050607081020304050607080 and the block is 1122334455667788. This is one of the test vectors from RFC 2040. The RFC lists it as a CBC test, where the plaintext 1020304050607080 is XORed with the IV 0102030405060708 first, and that produces exactly this block.
The table starts as S[0] = b7e15163 and S[1] = 5618cb1c, then continues in steps of Q32. After the three mixing passes, the first four table words are:
S[0] = 9b94df60 S[1] = 906fc5e5 S[2] = 0811617c S[3] = ba9a3f9b
The block’s little-endian words are A = 44332211 and B = 88776655. The key addition turns them into A = dfc80171 and B = 18e72c3a.
In round 1, B has low five bits equal to 26, so A ^ B is rotated left by that many places before S[2] is added. The new A has low five bits equal to 17, and that becomes the rotation for B. After round 1 the state is A = 372e1e31 and B = 1eb09f2d.
After 12 rounds the ciphertext is 294ddb46b3278d60. The same block and key with 8 rounds give c533771cd0110e63, and with 16 rounds they give dad6bda9dfe8f7e8. All three appear in RFC 2040.
RC6-32/20/16. The key is 0123456789abcdef0112233445566778 and the block is 02132435465768798a9bacbdcedfe0f1. The words are A = 35241302, B = 79685746, C = bdac9b8a, and D = f1e0dfce.
The key addition gives B = 7eaff47e and D = d684c550. In round 1 the quadratic function produces t = 471a10dd and u = 75a6ea0f. So A is rotated by 15 places and C by 29. After 20 rounds the ciphertext is:
524e192f4715c6231f51f6367ea43f18
With the 192-bit key 0123456789abcdef0112233445566778899aabbccddeeff0 the result is 688329d019e505041e52e92af95291d4. With the 256-bit key that appends 1032547698badcfe to it, the result is c8241816f0d7e48920ad16a1674e5d48. A block and key of all zero bytes encrypts to 8fc3a53656b1f778c129df4e9848a41e under RC6-32/20/16.
Python Implementation
This is a complete RC5 and RC6 for 32-bit words. It has the real key expansion, any key length, any round count for RC5, and the real RC6 quadratic function. Both ciphers share the same expand_key.
# rc5_rc6.py
#
# RC5 and RC6 with 32-bit words, written from Rivest's descriptions.
# RC5 encrypts a 64-bit block (two words) and takes any number of rounds
# and any key length. RC6, an AES finalist, encrypts a 128-bit block
# (four words), uses 20 rounds, and adds a multiplication to the round.
# Both use data-dependent rotations and the same key expansion.
MASK = 0xFFFFFFFF
P32 = 0xB7E15163 # derived from e
Q32 = 0x9E3779B9 # derived from the golden ratio
def rol(x, n):
n &= 31
return ((x << n) | (x >> (32 - n))) & MASK if n else x
def ror(x, n):
n &= 31
return ((x >> n) | (x << (32 - n))) & MASK if n else x
def expand_key(key, t):
"""Turn a key of any length into t round-key words."""
c = max(1, (len(key) + 3) // 4)
L = [0] * c
for i in reversed(range(len(key))):
L[i // 4] = ((L[i // 4] << 8) + key[i]) & MASK
S = [(P32 + i * Q32) & MASK for i in range(t)]
A = B = i = j = 0
for _ in range(3 * max(t, c)):
A = S[i] = rol((S[i] + A + B) & MASK, 3)
B = L[j] = rol((L[j] + A + B) & MASK, (A + B) & 31)
i = (i + 1) % t
j = (j + 1) % c
return S
def words(block):
return [int.from_bytes(block[i:i + 4], "little") for i in range(0, len(block), 4)]
def to_bytes(ws):
return b"".join(w.to_bytes(4, "little") for w in ws)
# ---- RC5 ----
def rc5_encrypt_block(key, block, rounds=12):
S = expand_key(key, 2 * rounds + 2)
A, B = words(block)
A = (A + S[0]) & MASK
B = (B + S[1]) & MASK
for i in range(1, rounds + 1):
A = (rol(A ^ B, B) + S[2 * i]) & MASK
B = (rol(B ^ A, A) + S[2 * i + 1]) & MASK
return to_bytes([A, B])
def rc5_decrypt_block(key, block, rounds=12):
S = expand_key(key, 2 * rounds + 2)
A, B = words(block)
for i in range(rounds, 0, -1):
B = ror((B - S[2 * i + 1]) & MASK, A) ^ A
A = ror((A - S[2 * i]) & MASK, B) ^ B
B = (B - S[1]) & MASK
A = (A - S[0]) & MASK
return to_bytes([A, B])
# ---- RC6 ----
def quad(x):
"""The RC6 mixing function: x * (2x + 1), then a rotate by 5."""
return rol((x * (2 * x + 1)) & MASK, 5)
def rc6_encrypt_block(key, block, rounds=20):
S = expand_key(key, 2 * rounds + 4)
A, B, C, D = words(block)
B = (B + S[0]) & MASK
D = (D + S[1]) & MASK
for i in range(1, rounds + 1):
t = quad(B)
u = quad(D)
A = (rol(A ^ t, u) + S[2 * i]) & MASK
C = (rol(C ^ u, t) + S[2 * i + 1]) & MASK
A, B, C, D = B, C, D, A
A = (A + S[2 * rounds + 2]) & MASK
C = (C + S[2 * rounds + 3]) & MASK
return to_bytes([A, B, C, D])
def rc6_decrypt_block(key, block, rounds=20):
S = expand_key(key, 2 * rounds + 4)
A, B, C, D = words(block)
C = (C - S[2 * rounds + 3]) & MASK
A = (A - S[2 * rounds + 2]) & MASK
for i in range(rounds, 0, -1):
A, B, C, D = D, A, B, C
u = quad(D)
t = quad(B)
C = ror((C - S[2 * i + 1]) & MASK, t) ^ u
A = ror((A - S[2 * i]) & MASK, u) ^ t
D = (D - S[1]) & MASK
B = (B - S[0]) & MASK
return to_bytes([A, B, C, D])
if __name__ == "__main__":
key = bytes.fromhex("0123456789abcdef0112233445566778")
block64 = bytes.fromhex("0123456789abcdef")
ct5 = rc5_encrypt_block(key, block64)
print(f"RC5-32/12/16 ciphertext: {ct5.hex()}")
print(f"RC5 recovered: {rc5_decrypt_block(key, ct5).hex()}")
block128 = bytes.fromhex("02132435465768798a9bacbdcedfe0f1")
ct6 = rc6_encrypt_block(key, block128)
print(f"RC6-32/20/16 ciphertext: {ct6.hex()}")
print(f"RC6 recovered: {rc6_decrypt_block(key, ct6).hex()}")
Running it produces this output:
RC5-32/12/16 ciphertext: 9b86752bd2a91b13
RC5 recovered: 0123456789abcdef
RC6-32/20/16 ciphertext: 524e192f4715c6231f51f6367ea43f18
RC6 recovered: 02132435465768798a9bacbdcedfe0f1
I checked this code against four independent sources before writing it up. It reproduces 27 test vectors from RFC 2040, which cover 0, 1, 2, 8, 12, and 16 rounds and keys from 1 to 16 bytes. It matches the RC5 and RC6 vectors in the libtomcrypt library, including RC6 with 128, 192, and 256-bit keys, plus the published all-zero RC6 vector. It agrees with OpenSSL’s RC5 on 60 random keys. Finally, libtomcrypt’s built-in RC6 starting table matches P32 + i × Q32 word for word.
For Fun: Both Ciphers in 23 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 122-line implementation above into 23 lines. The rotations, the key expansion, and every encrypt and decrypt routine are written as tight one-liners. It needs no imports and no other files.
MASK=0xFFFFFFFF;P32=0xB7E15163;Q32=0x9E3779B9;rol=lambda x,n: ((x<<(n&31))|(x>>(32-(n&31))))&MASK if n&31 else x;ror=lambda x,n: ((x>>(n&31))|(x<<(32-(n&31))))&MASK if n&31 else x;words=lambda b: [int.from_bytes(b[i:i+4],"little") for i in range(0,len(b),4)];to_bytes=lambda ws: b"".join(w.to_bytes(4,"little") for w in ws);quad=lambda x: rol((x*(2*x+1))&MASK,5)
def expand_key(key,t):
c=max(1,(len(key)+3)//4); L=[0]*c
for i in reversed(range(len(key))): L[i//4]=((L[i//4]<<8)+key[i])&MASK
S=[(P32+i*Q32)&MASK for i in range(t)]; A=B=i=j=0
for _ in range(3*max(t,c)): A=S[i]=rol((S[i]+A+B)&MASK,3); B=L[j]=rol((L[j]+A+B)&MASK,(A+B)&31); i=(i+1)%t; j=(j+1)%c
return S
def rc5_encrypt_block(key,block,rounds=12):
S=expand_key(key,2*rounds+2); A,B=words(block); A=(A+S[0])&MASK; B=(B+S[1])&MASK
for i in range(1,rounds+1): A=(rol(A^B,B)+S[2*i])&MASK; B=(rol(B^A,A)+S[2*i+1])&MASK
return to_bytes([A,B])
def rc5_decrypt_block(key,block,rounds=12):
S=expand_key(key,2*rounds+2); A,B=words(block)
for i in range(rounds,0,-1): B=ror((B-S[2*i+1])&MASK,A)^A; A=ror((A-S[2*i])&MASK,B)^B
B=(B-S[1])&MASK; A=(A-S[0])&MASK; return to_bytes([A,B])
def rc6_encrypt_block(key,block,rounds=20):
S=expand_key(key,2*rounds+4); A,B,C,D=words(block); B=(B+S[0])&MASK; D=(D+S[1])&MASK
for i in range(1,rounds+1): t=quad(B); u=quad(D); A=(rol(A^t,u)+S[2*i])&MASK; C=(rol(C^u,t)+S[2*i+1])&MASK; A,B,C,D=B,C,D,A
A=(A+S[2*rounds+2])&MASK; C=(C+S[2*rounds+3])&MASK; return to_bytes([A,B,C,D])
def rc6_decrypt_block(key,block,rounds=20):
S=expand_key(key,2*rounds+4); A,B,C,D=words(block); C=(C-S[2*rounds+3])&MASK; A=(A-S[2*rounds+2])&MASK
for i in range(rounds,0,-1): A,B,C,D=D,A,B,C; u=quad(D); t=quad(B); C=ror((C-S[2*i+1])&MASK,t)^u; A=ror((A-S[2*i])&MASK,u)^t
D=(D-S[1])&MASK; B=(B-S[0])&MASK; return to_bytes([A,B,C,D])
if __name__=="__main__": key=bytes.fromhex("0123456789abcdef0112233445566778"); block64=bytes.fromhex("0123456789abcdef"); ct5=rc5_encrypt_block(key,block64); print(f"RC5-32/12/16 ciphertext: {ct5.hex()}"); print(f"RC5 recovered: {rc5_decrypt_block(key,ct5).hex()}"); block128=bytes.fromhex("02132435465768798a9bacbdcedfe0f1"); ct6=rc6_encrypt_block(key,block128); print(f"RC6-32/20/16 ciphertext: {ct6.hex()}"); print(f"RC6 recovered: {rc6_decrypt_block(key,ct6).hex()}")
Running it prints the same four lines as the readable version, including the RC6 ciphertext from the published test vectors:
RC5-32/12/16 ciphertext: 9b86752bd2a91b13
RC5 recovered: 0123456789abcdef
RC6-32/20/16 ciphertext: 524e192f4715c6231f51f6367ea43f18
RC6 recovered: 02132435465768798a9bacbdcedfe0f1
I checked it against the readable code on 1,100 combinations of random keys and blocks. The keys ranged from 0 to 64 bytes, and the round counts were 0, 1, 12, 16, and 20, for both ciphers. Encryption matched every time, and decryption recovered every block. It also reproduces the RFC 2040 ciphertext for RC5-32/12/16 and the all-zero RC6 test vector.
Limitations
This is a faithful teaching version of the ciphers, not a production one:
- Single blocks only. There is no mode of operation for longer messages and no padding for partial blocks.
- Variable-time rotations on some hardware. A rotation by a secret amount can leak through timing on processors whose rotate instruction takes a variable number of cycles. Constant-time code is needed in practice.
- 32-bit words only. The RC5 family also allows 16 and 64-bit words, which this code leaves out to stay readable.
- No side-channel hardening. There is no protection against power or fault attacks.
Security Status
RC5 with 12 rounds is vulnerable to a differential attack that needs about 2⁴⁴ chosen plaintexts. That attack is one reason to use more than 12 rounds, and it shows how much the rotation structure does and does not protect. With more rounds, no practical attack on the full cipher is known. The 64-bit block of RC5-32 is also a limit for large volumes of data, for the same birthday-bound reasons covered in the 3DES guide.
RC6 was designed to fix RC5’s weaknesses. It has a 128-bit block, 20 rounds, and the multiplication that spreads each word into the rotation amounts. No practical attack on the full 20-round cipher is publicly known. RC6 was one of the five AES finalists, but NIST chose Rijndael instead.
FAQ
What is the difference between RC5 and RC6?
RC5 encrypts a 64-bit block with two words. RC6 encrypts a 128-bit block with four words and adds a multiplication to the rotation amounts. RC6 can be seen as two interleaved RC5 processes.
Why are the rotations data-dependent?
It makes the cipher harder to analyze. Attacks that track how bits move through a cipher struggle when the rotation distance changes with every block. Rivest also wanted the community to study the idea.
Is RC5 still safe?
With enough rounds and a short message volume, nothing practical breaks it. But its 64-bit block and the known differential attack on 12 rounds mean new designs should use AES instead.
Why did RC6 not become AES?
NIST chose Rijndael as the best overall balance of security, speed, and flexibility across many platforms. RC6 was a strong finalist, and no practical attack against it is known.
What does RC5-32/12/16 mean?
It names a parameter set: 32-bit words, 12 rounds, and a 16-byte key. The same notation describes RC5 and RC6 with other parameters.
References
-
Rivest, R. “The RC5 Encryption Algorithm.” Fast Software Encryption (FSE) 1994.
-
Rivest, R., Robshaw, M., Sidney, R., and Yin, Y. “The RC6 Block Cipher.” Submission to the AES competition, 1998.
-
Baldwin, R. and Rivest, R. “The RC5, RC5-CBC, RC5-CBC-Pad, and RC5-CTS Algorithms.” RFC 2040, 1996. Available at: https://www.rfc-editor.org/rfc/rfc2040
-
Biryukov, A. and Kushilevitz, E. “Improved Cryptanalysis of RC5.” EUROCRYPT 1998.
-
Wikipedia. “RC5.” Available at: https://en.wikipedia.org/wiki/RC5
-
Wikipedia. “RC6.” Available at: https://en.wikipedia.org/wiki/RC6