The Skipjack Algorithm
Skipjack was the secret NSA cipher inside the Clipper chip, declassified in 1998. Learn how its 32 rounds of alternating rules work on four 16-bit words, and why its 80-bit key and its history make it a cautionary tale.
Interactive Skipjack Encryption
🔐 Skipjack Encryption
The Skipjack Algorithm
Introduction
Skipjack is a block cipher designed by the United States National Security Agency. It encrypts 64-bit blocks with an 80-bit key through 32 rounds, and it was built for secure telephones. Skipjack is best known as the cipher inside the Clipper chip, the centerpiece of a 1990s government plan to let law enforcement recover encrypted conversations.
The algorithm was kept secret for years, and that secrecy shaped its reputation. When it was finally declassified in 1998, cryptographers had a rare chance to study an NSA design in full. What they found was a small, unusual cipher that has held up well, but whose short key and troubled history keep it out of modern use.
Table of Contents
- History
- How Skipjack Works
- The G Permutation
- The F Table
- Key Schedule
- Byte Order
- A Worked Example
- Python Implementation
- Limitations
- Security Status
- FAQ
- References
History
The NSA developed Skipjack, with a first design in 1987 and roots reaching back to around 1980. The government provided it for use in the Clipper chip, which was implemented in tamper-resistant hardware. Clipper was part of a key escrow scheme: a separate mechanism called the Law Enforcement Access Field let authorized agencies recover session keys. That escrow was a feature of the system around Skipjack, not of the cipher itself. The scheme was standardized as FIPS 185, the Escrowed Encryption Standard.
An independent evaluation panel reviewed the algorithm: Ernest Brickell, Dorothy Denning, Stephen Kent, David Maher, and Walter Tuchman. They found no problems with it or with the evaluation process. The NSA declassified Skipjack on June 24, 1998. Cryptanalysts went to work immediately: Biham and Shamir attacked 16 of the 32 rounds within one day of the declassification. NIST later recommended against using Skipjack after 2010.
How Skipjack Works
Skipjack treats the 64-bit block as four 16-bit words, w1 to w4. It runs 32 rounds, and every round uses one of two update rules. Rule A is used in rounds 1 to 8 and 17 to 24. Rule B is used in rounds 9 to 16 and 25 to 32. A round counter, which runs from 1 to 32, is mixed into each round.
Rule A: w1' = G(w1) ^ w4 ^ counter Rule B: w1' = w4
w2' = G(w1) w2' = G(w1)
w3' = w2 w3' = w1 ^ w2 ^ counter
w4' = w3 w4' = w3
Only w1 goes through the nonlinear function G in each round, and the words then shuffle positions. That makes Skipjack an unbalanced Feistel network: one word is transformed while the others mostly move. Decryption undoes each rule in reverse order, using the inverse of G.
Interactive Visualizer
The visualizer above runs this exact algorithm. Each row of the log shows the four words after a round, and the note names the rule, the counter, and the four key bytes that G used.
The G Permutation
The function G turns a 16-bit word into another 16-bit word. It splits the word into a high byte and a low byte and runs a small four-step Feistel network on them. Each step looks up a byte in the table F and mixes in one key byte:
g1 = high byte of w, g2 = low byte of w
g3 = g1 ^ F[g2 ^ key byte 0]
g4 = g2 ^ F[g3 ^ key byte 1]
g5 = g3 ^ F[g4 ^ key byte 2]
g6 = g4 ^ F[g5 ^ key byte 3]
G(w) = g5 followed by g6
Each step is easy to reverse, because XORing the same table output a second time cancels it. The inverse G simply runs the four steps backward. Since every round runs G once, a full encryption runs it 32 times, using 128 key bytes in total.
The F Table
F is a fixed table of 256 bytes. It is a permutation of the numbers 0 to 255, so every input byte maps to a different output byte. For example, F[0x00] = 0xa3 and F[0xff] = 0x46. The specification presents it as a plain table, and the code below copies it from a published implementation.
Key Schedule
Skipjack has no key schedule in the usual sense. The 10-byte key is used directly, over and over. Each call to G consumes four consecutive key bytes, and the key bytes wrap around after the tenth. Round k (counting from zero) uses key bytes number 4k, 4k + 1, 4k + 2, and 4k + 3, each taken modulo 10.
That cycle has period 5 rounds, since four bytes per round and ten key bytes line up again after five rounds. The design is simple and fast, which suited small hardware, but it gives an attacker a very regular structure to study.
Byte Order
Different sources print Skipjack’s test data in different byte orders, and mixing them up is a common source of confusion. The NIST specification writes the key and block in the order used throughout this guide, with w1 and the first key byte first.
The NIST known-answer test file, SP 800-17, prints the same data with the bytes in reverse order, as if each value were a single big number. The code here follows the specification. When I tested against the SP 800-17 vectors, I reversed their bytes first, and every one matched.
A Worked Example
This is the example from the NIST specification. The key is 00998877665544332211 and the plaintext is 33221100ddccbbaa. The plaintext words are 3322 1100 ddcc bbaa.
Round 1 (rule A, counter 1). G takes w1 = 3322, so g1 = 33 and g2 = 22. It uses key bytes 00, 99, 88, and 77:
g3 = g1 ^ F[g2 ^ 00 = 22] = 33 ^ 02 = 31
g4 = g2 ^ F[g3 ^ 99 = a8] = 22 ^ 50 = 72
g5 = g3 ^ F[g4 ^ 88 = fa] = 31 ^ 3a = 0b
g6 = g4 ^ F[g5 ^ 77 = 7c] = 72 ^ dd = af
That gives G(w1) = g5 g6 = 0baf for this round. Rule A then gives w1' = G(w1) ^ w4 ^ 1 = b004, with w2' = 0baf, w3' = 1100, and w4' = ddcc. The state after round 1 is:
b004 0baf 1100 ddcc
Later rounds. After round 2 the state is e688 3b46 0baf 1100. After round 8, the last round of the first rule A block, it is d79b 5599 be50 dd90. Round 9 switches to rule B and gives dd90 1e0b 820b be50. After round 16 it is d7f8 8990 5397 9883, after round 24 it is 65a7 deaa 1115 45c0, and after round 31 it is 1adc 60ee d300 2587.
Result. After round 32 the four words are 2587 cae2 7a12 d300, so the ciphertext is:
2587cae27a12d300
Python Implementation
This is a complete Skipjack: the real G permutation, both update rules, the round counter, and decryption through the inverse rules. The F table was copied by a script from the libtomcrypt library, and the script confirmed that it matches the table in a second implementation, Crypto++, entry for entry.
# skipjack.py
#
# Skipjack, the NSA cipher designed for the Clipper chip and declassified in
# 1998. A 64-bit block, an 80-bit key, and 32 rounds of an unbalanced Feistel
# network on four 16-bit words. Rounds 1-8 and 17-24 use rule A; rounds
# 9-16 and 25-32 use rule B. Each round runs the first word through G, a
# four-step Feistel permutation built from the 8-bit table F and the key.
F = bytes([
0xa3, 0xd7, 0x09, 0x83, 0xf8, 0x48, 0xf6, 0xf4,
0xb3, 0x21, 0x15, 0x78, 0x99, 0xb1, 0xaf, 0xf9,
0xe7, 0x2d, 0x4d, 0x8a, 0xce, 0x4c, 0xca, 0x2e,
0x52, 0x95, 0xd9, 0x1e, 0x4e, 0x38, 0x44, 0x28,
0x0a, 0xdf, 0x02, 0xa0, 0x17, 0xf1, 0x60, 0x68,
0x12, 0xb7, 0x7a, 0xc3, 0xe9, 0xfa, 0x3d, 0x53,
0x96, 0x84, 0x6b, 0xba, 0xf2, 0x63, 0x9a, 0x19,
0x7c, 0xae, 0xe5, 0xf5, 0xf7, 0x16, 0x6a, 0xa2,
0x39, 0xb6, 0x7b, 0x0f, 0xc1, 0x93, 0x81, 0x1b,
0xee, 0xb4, 0x1a, 0xea, 0xd0, 0x91, 0x2f, 0xb8,
0x55, 0xb9, 0xda, 0x85, 0x3f, 0x41, 0xbf, 0xe0,
0x5a, 0x58, 0x80, 0x5f, 0x66, 0x0b, 0xd8, 0x90,
0x35, 0xd5, 0xc0, 0xa7, 0x33, 0x06, 0x65, 0x69,
0x45, 0x00, 0x94, 0x56, 0x6d, 0x98, 0x9b, 0x76,
0x97, 0xfc, 0xb2, 0xc2, 0xb0, 0xfe, 0xdb, 0x20,
0xe1, 0xeb, 0xd6, 0xe4, 0xdd, 0x47, 0x4a, 0x1d,
0x42, 0xed, 0x9e, 0x6e, 0x49, 0x3c, 0xcd, 0x43,
0x27, 0xd2, 0x07, 0xd4, 0xde, 0xc7, 0x67, 0x18,
0x89, 0xcb, 0x30, 0x1f, 0x8d, 0xc6, 0x8f, 0xaa,
0xc8, 0x74, 0xdc, 0xc9, 0x5d, 0x5c, 0x31, 0xa4,
0x70, 0x88, 0x61, 0x2c, 0x9f, 0x0d, 0x2b, 0x87,
0x50, 0x82, 0x54, 0x64, 0x26, 0x7d, 0x03, 0x40,
0x34, 0x4b, 0x1c, 0x73, 0xd1, 0xc4, 0xfd, 0x3b,
0xcc, 0xfb, 0x7f, 0xab, 0xe6, 0x3e, 0x5b, 0xa5,
0xad, 0x04, 0x23, 0x9c, 0x14, 0x51, 0x22, 0xf0,
0x29, 0x79, 0x71, 0x7e, 0xff, 0x8c, 0x0e, 0xe2,
0x0c, 0xef, 0xbc, 0x72, 0x75, 0x6f, 0x37, 0xa1,
0xec, 0xd3, 0x8e, 0x62, 0x8b, 0x86, 0x10, 0xe8,
0x08, 0x77, 0x11, 0xbe, 0x92, 0x4f, 0x24, 0xc5,
0x32, 0x36, 0x9d, 0xcf, 0xf3, 0xa6, 0xbb, 0xac,
0x5e, 0x6c, 0xa9, 0x13, 0x57, 0x25, 0xb5, 0xe3,
0xbd, 0xa8, 0x3a, 0x01, 0x05, 0x59, 0x2a, 0x46,
])
def g(w, k, key):
"""G permutation for round k (counting from 0): four table-driven steps."""
left, right = w >> 8, w & 255
for i in range(4):
cv = key[(4 * k + i) % 10]
left, right = right, left ^ F[right ^ cv]
return (left << 8) | right
def g_inv(w, k, key):
left, right = w >> 8, w & 255
for i in reversed(range(4)):
cv = key[(4 * k + i) % 10]
left, right = right ^ F[left ^ cv], left
return (left << 8) | right
def rule_a(w, k, key):
counter = k + 1
t = g(w[0], k, key)
return [t ^ w[3] ^ counter, t, w[1], w[2]]
def rule_b(w, k, key):
counter = k + 1
t = g(w[0], k, key)
return [w[3], t, w[0] ^ w[1] ^ counter, w[2]]
def rule_a_inv(w, k, key):
counter = k + 1
return [g_inv(w[1], k, key), w[2], w[3], w[0] ^ w[1] ^ counter]
def rule_b_inv(w, k, key):
counter = k + 1
w1 = g_inv(w[1], k, key)
return [w1, w[2] ^ w1 ^ counter, w[3], w[0]]
def uses_rule_a(k):
return (k // 8) % 2 == 0
def encrypt_block(key, block):
if len(key) != 10:
raise ValueError("Skipjack keys are 10 bytes")
w = [int.from_bytes(block[2 * i:2 * i + 2], "big") for i in range(4)]
for k in range(32):
w = rule_a(w, k, key) if uses_rule_a(k) else rule_b(w, k, key)
return b"".join(x.to_bytes(2, "big") for x in w)
def decrypt_block(key, block):
w = [int.from_bytes(block[2 * i:2 * i + 2], "big") for i in range(4)]
for k in reversed(range(32)):
w = rule_a_inv(w, k, key) if uses_rule_a(k) else rule_b_inv(w, k, key)
return b"".join(x.to_bytes(2, "big") for x in w)
if __name__ == "__main__":
key = bytes.fromhex("00998877665544332211")
plaintext = bytes.fromhex("33221100ddccbbaa")
ciphertext = encrypt_block(key, plaintext)
recovered = decrypt_block(key, ciphertext)
print(f"Key: {key.hex()}")
print(f"Plaintext: {plaintext.hex()}")
print(f"Ciphertext: {ciphertext.hex()}")
print(f"Recovered: {recovered.hex()}")
Running it produces this output:
Key: 00998877665544332211
Plaintext: 33221100ddccbbaa
Ciphertext: 2587cae27a12d300
Recovered: 33221100ddccbbaa
I checked this code against three sources before writing it up. It reproduces the example from the NIST specification. It matches all 80 of the known-answer tests in NIST SP 800-17, which set each of the 80 key bits in turn against an all-zero plaintext, and all 80 CBC vectors from the Botan library, which cover 440 blocks. Both of those sources print bytes in reverse order, so I reversed them before comparing. Finally, 1,000 encryptions followed by 1,000 decryptions of a zero block return exactly to zero.
For Fun: The Same Cipher 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 115-line implementation above into 23 lines. The F table takes one line, and the G permutation, the two rules, and their inverses are written as tight one-liners. It needs no imports and no other files.
F=bytes([0xa3,0xd7,0x09,0x83,0xf8,0x48,0xf6,0xf4,0xb3,0x21,0x15,0x78,0x99,0xb1,0xaf,0xf9,0xe7,0x2d,0x4d,0x8a,0xce,0x4c,0xca,0x2e,0x52,0x95,0xd9,0x1e,0x4e,0x38,0x44,0x28,0x0a,0xdf,0x02,0xa0,0x17,0xf1,0x60,0x68,0x12,0xb7,0x7a,0xc3,0xe9,0xfa,0x3d,0x53,0x96,0x84,0x6b,0xba,0xf2,0x63,0x9a,0x19,0x7c,0xae,0xe5,0xf5,0xf7,0x16,0x6a,0xa2,0x39,0xb6,0x7b,0x0f,0xc1,0x93,0x81,0x1b,0xee,0xb4,0x1a,0xea,0xd0,0x91,0x2f,0xb8,0x55,0xb9,0xda,0x85,0x3f,0x41,0xbf,0xe0,0x5a,0x58,0x80,0x5f,0x66,0x0b,0xd8,0x90,0x35,0xd5,0xc0,0xa7,0x33,0x06,0x65,0x69,0x45,0x00,0x94,0x56,0x6d,0x98,0x9b,0x76,0x97,0xfc,0xb2,0xc2,0xb0,0xfe,0xdb,0x20,0xe1,0xeb,0xd6,0xe4,0xdd,0x47,0x4a,0x1d,0x42,0xed,0x9e,0x6e,0x49,0x3c,0xcd,0x43,0x27,0xd2,0x07,0xd4,0xde,0xc7,0x67,0x18,0x89,0xcb,0x30,0x1f,0x8d,0xc6,0x8f,0xaa,0xc8,0x74,0xdc,0xc9,0x5d,0x5c,0x31,0xa4,0x70,0x88,0x61,0x2c,0x9f,0x0d,0x2b,0x87,0x50,0x82,0x54,0x64,0x26,0x7d,0x03,0x40,0x34,0x4b,0x1c,0x73,0xd1,0xc4,0xfd,0x3b,0xcc,0xfb,0x7f,0xab,0xe6,0x3e,0x5b,0xa5,0xad,0x04,0x23,0x9c,0x14,0x51,0x22,0xf0,0x29,0x79,0x71,0x7e,0xff,0x8c,0x0e,0xe2,0x0c,0xef,0xbc,0x72,0x75,0x6f,0x37,0xa1,0xec,0xd3,0x8e,0x62,0x8b,0x86,0x10,0xe8,0x08,0x77,0x11,0xbe,0x92,0x4f,0x24,0xc5,0x32,0x36,0x9d,0xcf,0xf3,0xa6,0xbb,0xac,0x5e,0x6c,0xa9,0x13,0x57,0x25,0xb5,0xe3,0xbd,0xa8,0x3a,0x01,0x05,0x59,0x2a,0x46])
def g(w,k,key):
l,r=w>>8,w&255
for i in range(4): c=key[(4*k+i)%10]; l,r=r,l^F[r^c]
return (l<<8)|r
def g_inv(w,k,key):
l,r=w>>8,w&255
for i in range(3,-1,-1): c=key[(4*k+i)%10]; l,r=r^F[l^c],l
return (l<<8)|r
def rule_a(w,k,key): t=g(w[0],k,key); return [t^w[3]^k+1,t,w[1],w[2]]
def rule_b(w,k,key): t=g(w[0],k,key); return [w[3],t,w[0]^w[1]^k+1,w[2]]
def rule_a_inv(w,k,key): return [g_inv(w[1],k,key),w[2],w[3],w[0]^w[1]^k+1]
def rule_b_inv(w,k,key): w1=g_inv(w[1],k,key); return [w1,w[2]^w1^k+1,w[3],w[0]]
def uses_a(k): return (k//8)%2==0
def encrypt_block(key,block):
if len(key)!=10: raise ValueError("Skipjack keys are 10 bytes")
w=[int.from_bytes(block[2*i:2*i+2],"big") for i in range(4)]
for k in range(32): w=rule_a(w,k,key) if uses_a(k) else rule_b(w,k,key)
return b"".join(x.to_bytes(2,"big") for x in w)
def decrypt_block(key,block):
w=[int.from_bytes(block[2*i:2*i+2],"big") for i in range(4)]
for k in range(31,-1,-1): w=rule_a_inv(w,k,key) if uses_a(k) else rule_b_inv(w,k,key)
return b"".join(x.to_bytes(2,"big") for x in w)
key=bytes.fromhex("00998877665544332211"); pt=bytes.fromhex("33221100ddccbbaa"); ct=encrypt_block(key,pt); rec=decrypt_block(key,ct); print(f"Key: {key.hex()}"); print(f"Plaintext: {pt.hex()}"); print(f"Ciphertext: {ct.hex()}"); print(f"Recovered: {rec.hex()}")
Running it prints the same four lines as the readable version, including the NIST test vector ciphertext:
Key: 00998877665544332211
Plaintext: 33221100ddccbbaa
Ciphertext: 2587cae27a12d300
Recovered: 33221100ddccbbaa
I checked it against the readable code on 500 random keys and blocks. Encryption matched every time, and decryption recovered every block. The F table is identical, and it reproduces the NIST test vector. Keys of the wrong length raise the same error.
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 key is short. An 80-bit key is too small for long-term security by modern standards.
- Table lookups depend on secret data. The
Flookups use values that depend on the key and data, so real software must consider cache-timing leaks that this plain code does not address. - A 64-bit block is also a limit. Large volumes of data under one key invite the birthday-bound problems described in the 3DES guide.
Security Status
No known attack on the full 32-round cipher is faster than exhaustive key search by a meaningful margin. The strongest published result is an impossible differential attack by Biham, Biryukov, and Shamir on 31 of the 32 rounds, and it is only slightly faster than trying every key. An independent panel also reviewed the design before it was released.
The problems with Skipjack are about its key and its context, not about a break. An 80-bit key gives only 2⁸⁰ possible keys, which is considered too small for long-term security by modern standards. NIST recommended against using Skipjack after 2010, and it later drafted standards that no longer certify the cipher for US government use. Most of the lasting interest in Skipjack is historical: it shows what a government-designed cipher looked like once the secrecy was lifted.
FAQ
Was Skipjack broken?
Not in any practical sense. Attacks on reduced-round versions exist, including one on 31 of 32 rounds, but none threatens the full cipher. Its weakness is the short 80-bit key, not a flaw in the design.
What was the Clipper chip?
It was a tamper-resistant chip for encrypted telephones, built around Skipjack. It came with a key escrow scheme that let authorized agencies recover keys, which made it controversial. The escrow mechanism was separate from the cipher.
Why was Skipjack kept secret?
The NSA classified the design, and it was declassified on June 24, 1998. During the secret period, an independent evaluation panel reviewed it and found no problems.
How is Skipjack different from DES?
DES uses 16 rounds on two 32-bit halves and a 56-bit key. Skipjack uses 32 rounds on four 16-bit words and an 80-bit key. Skipjack is an unbalanced Feistel network with two alternating update rules, and DES is a balanced Feistel network with one.
Can I use Skipjack today?
You should not use it today. NIST recommended against it after 2010, and AES is a better choice in every respect. Skipjack is worth studying as a piece of history and as an example of an unbalanced Feistel design.
References
-
National Institute of Standards and Technology. “SKIPJACK and KEA Algorithm Specifications.” Version 2.0, 1998.
-
National Institute of Standards and Technology. “Escrowed Encryption Standard (EES).” FIPS PUB 185, 1994.
-
Biham, E., Biryukov, A., and Shamir, A. “Cryptanalysis of Skipjack Reduced to 31 Rounds Using Impossible Differentials.” EUROCRYPT 1999.
-
National Institute of Standards and Technology. “Modes of Operation Validation System (MOVS): Requirements and Procedures.” Special Publication 800-17, 1998.
-
Wikipedia. “Skipjack (cipher).” Available at: https://en.wikipedia.org/wiki/Skipjack_(cipher)