Type any 16-character text below. The demo shows your message as a 4 × 4 byte grid (the AES state), then walks you through the four steps of a single AES round one click at a time.
<div class="hint">Type 16 characters, then step through a single AES round.</div>
<div class="input-row">
<label for="msg">Plaintext (16 chars):</label>
<input id="msg" type="text" maxlength="16" value="Hello, AES World" autocomplete="off" spellcheck="false"/>
</div>
<div class="step-label" id="step-label">State (input bytes)</div>
<div id="grid" class="grid"></div>
<div class="legend" id="legend"></div>
<div class="btns">
<button id="btn-sub" type="button">SubBytes</button>
<button id="btn-shift" type="button">ShiftRows</button>
<button id="btn-mix" type="button">MixColumns</button>
<button id="btn-key" type="button">AddRoundKey</button>
<button id="btn-reset" type="button" class="ghost">Reset</button>
</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.hint { font-size: .85rem; color: #555; margin: 0 0 .6rem; }
.input-row { display: flex; align-items: center; gap: .5rem; margin-bottom: .7rem; flex-wrap: wrap; }
label { font-size: .9rem; }
input { font: 14px ui-monospace, monospace; padding: .3rem .5rem; border: 1px solid #adb1b8;
border-radius: 6px; width: 170px; }
.step-label { font-size: .8rem; font-weight: 700; letter-spacing: .06em; text-transform: uppercase;
color: #5a6a7a; margin-bottom: .35rem; }
.grid { display: grid; grid-template-columns: repeat(4, 52px); gap: 4px; margin-bottom: .6rem; }
.cell { width: 52px; height: 52px; display: flex; flex-direction: column; align-items: center;
justify-content: center; border-radius: 7px; font: 700 13px ui-monospace, monospace;
transition: background .35s, color .35s; user-select: none; }
.cell .hex { font-size: 14px; }
.cell .ch { font-size: 10px; color: inherit; opacity: .7; margin-top: 1px; }
.state-init { background: #e8eef3; color: #1d3557; border: 1px solid #cdd9e3; }
.state-sub { background: #fde8c0; color: #7a3f00; border: 1px solid #f4c77a; }
.state-shift { background: #d4f0e0; color: #0a4d28; border: 1px solid #74c99a; }
.state-mix { background: #dce8ff; color: #1a3584; border: 1px solid #7fa6f4; }
.state-key { background: #f0d4f0; color: #4d0a5e; border: 1px solid #c97fcc; }
.legend { font-size: .8rem; color: #555; margin-bottom: .5rem; min-height: 1.3em; }
.btns { display: flex; gap: .45rem; flex-wrap: wrap; }
button { font: 600 13px system-ui; padding: .4rem .85rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 7px; cursor: pointer; }
button:disabled { opacity: .38; cursor: default; }
button.ghost { background: #fff; color: #1d3557; }
button.ghost:disabled { opacity: .38; }
// AES S-box (FIPS 197, Figure 7)
const SBOX = [
0x63,0x7c,0x77,0x7b,0xf2,0x6b,0x6f,0xc5,0x30,0x01,0x67,0x2b,0xfe,0xd7,0xab,0x76,
0xca,0x82,0xc9,0x7d,0xfa,0x59,0x47,0xf0,0xad,0xd4,0xa2,0xaf,0x9c,0xa4,0x72,0xc0,
0xb7,0xfd,0x93,0x26,0x36,0x3f,0xf7,0xcc,0x34,0xa5,0xe5,0xf1,0x71,0xd8,0x31,0x15,
0x04,0xc7,0x23,0xc3,0x18,0x96,0x05,0x9a,0x07,0x12,0x80,0xe2,0xeb,0x27,0xb2,0x75,
0x09,0x83,0x2c,0x1a,0x1b,0x6e,0x5a,0xa0,0x52,0x3b,0xd6,0xb3,0x29,0xe3,0x2f,0x84,
0x53,0xd1,0x00,0xed,0x20,0xfc,0xb1,0x5b,0x6a,0xcb,0xbe,0x39,0x4a,0x4c,0x58,0xcf,
0xd0,0xef,0xaa,0xfb,0x43,0x4d,0x33,0x85,0x45,0xf9,0x02,0x7f,0x50,0x3c,0x9f,0xa8,
0x51,0xa3,0x40,0x8f,0x92,0x9d,0x38,0xf5,0xbc,0xb6,0xda,0x21,0x10,0xff,0xf3,0xd2,
0xcd,0x0c,0x13,0xec,0x5f,0x97,0x44,0x17,0xc4,0xa7,0x7e,0x3d,0x64,0x5d,0x19,0x73,
0x60,0x81,0x4f,0xdc,0x22,0x2a,0x90,0x88,0x46,0xee,0xb8,0x14,0xde,0x5e,0x0b,0xdb,
0xe0,0x32,0x3a,0x0a,0x49,0x06,0x24,0x5c,0xc2,0xd3,0xac,0x62,0x91,0x95,0xe4,0x79,
0xe7,0xc8,0x37,0x6d,0x8d,0xd5,0x4e,0xa9,0x6c,0x56,0xf4,0xea,0x65,0x7a,0xae,0x08,
0xba,0x78,0x25,0x2e,0x1c,0xa6,0xb4,0xc6,0xe8,0xdd,0x74,0x1f,0x4b,0xbd,0x8b,0x8a,
0x70,0x3e,0xb5,0x66,0x48,0x03,0xf6,0x0e,0x61,0x35,0x57,0xb9,0x86,0xc1,0x1d,0x9e,
0xe1,0xf8,0x98,0x11,0x69,0xd9,0x8e,0x94,0x9b,0x1e,0x87,0xe9,0xce,0x55,0x28,0xdf,
0x8c,0xa1,0x89,0x0d,0xbf,0xe6,0x42,0x68,0x41,0x99,0x2d,0x0f,0xb0,0x54,0xbb,0x16
];
// AES state is column-major (bytes fill down columns first).
// We display row-major (left to right, top to bottom) for readability.
// state[col][row] = byte.
// Fixed round key (demo only — not a real key schedule)
const ROUND_KEY = [
[0x2b,0x28,0xab,0x09],[0x7e,0xae,0xf7,0xcf],
[0x15,0xd2,0x15,0x4f],[0x16,0xa6,0x88,0x3c]
];
// GF(2^8) multiply by 2 (xtime)
function xtime(b){ return ((b<<1)^(b&0x80?0x1b:0))&0xff; }
// GF multiply
function gmul(a,b){
let p=0; for(let i=0;i<8;i++){
if(b&1) p^=a;
const hi=a&0x80; a=(a<<1)&0xff; if(hi) a^=0x1b; b>>=1;
} return p;
}
// Convert message string -> 4x4 state (column-major: state[col][row])
function msgToState(msg){
const bytes=[]; for(let i=0;i<16;i++) bytes.push(msg.charCodeAt(i)||0x20);
// Fill column-major
const s=[[],[],[],[]];
for(let col=0;col<4;col++) for(let row=0;row<4;row++) s[col][row]=bytes[row*4+col];
return s;
}
// Display helpers
function toHex(b){ return b.toString(16).padStart(2,'0').toUpperCase(); }
function toPrint(b){ return (b>=32&&b<127)?String.fromCharCode(b):'·'; }
let state=[], phase=0;
// phases: 0=init, 1=subbytes, 2=shiftrows, 3=mixcols, 4=addroundkey
function getInput(){
let m=document.getElementById('msg').value;
while(m.length<16) m+=' ';
return m.slice(0,16);
}
function deepCopy(s){ return s.map(col=>[...col]); }
function reset(){
phase=0;
state=msgToState(getInput());
render('init','State (input bytes) — column-major layout, row ↔ column swapped for display');
setButtons();
}
function renderGrid(s, cls){
const g=document.getElementById('grid'); g.innerHTML='';
// Display row-major: row r, col c -> byte s[c][r]
for(let r=0;r<4;r++) for(let c=0;c<4;c++){
const b=s[c][r];
const d=document.createElement('div');
d.className='cell '+cls;
d.innerHTML='<span class="hex">'+toHex(b)+'</span><span class="ch">'+toPrint(b)+'</span>';
g.appendChild(d);
}
}
function render(cls, label){
renderGrid(state, 'state-'+cls);
document.getElementById('step-label').textContent=label;
}
function setLegend(t){ document.getElementById('legend').textContent=t; }
function setButtons(){
const ids=['btn-sub','btn-shift','btn-mix','btn-key'];
// enable only the next step
ids.forEach((id,i)=>{ document.getElementById(id).disabled=(i!==phase); });
}
function doSubBytes(){
state=deepCopy(state);
for(let c=0;c<4;c++) for(let r=0;r<4;r++) state[c][r]=SBOX[state[c][r]];
phase=1; render('sub','After SubBytes — each byte replaced by S-box lookup');
setLegend('S-box maps every byte to a nonlinear substitute, breaking algebraic patterns.');
setButtons();
}
function doShiftRows(){
// Operate on rows: row r shifts left by r positions.
// Extract rows, shift, put back.
state=deepCopy(state);
for(let r=1;r<4;r++){
const row=[state[0][r],state[1][r],state[2][r],state[3][r]];
for(let c=0;c<4;c++) state[c][r]=row[(c+r)%4];
}
phase=2; render('shift','After ShiftRows — row 0 stays, row 1 shifts left 1, etc.');
setLegend('Bytes spread across all four columns — no column is left unchanged.');
setButtons();
}
function doMixColumns(){
state=deepCopy(state);
for(let c=0;c<4;c++){
const [s0,s1,s2,s3]=state[c];
state[c][0]=gmul(s0,2)^gmul(s1,3)^s2^s3;
state[c][1]=s0^gmul(s1,2)^gmul(s2,3)^s3;
state[c][2]=s0^s1^gmul(s2,2)^gmul(s3,3);
state[c][3]=gmul(s0,3)^s1^s2^gmul(s3,2);
}
phase=3; render('mix','After MixColumns — each column mixed via GF(2⁸) polynomial');
setLegend('One byte change in a column now affects all four bytes — full column diffusion.');
setButtons();
}
function doAddRoundKey(){
state=deepCopy(state);
for(let c=0;c<4;c++) for(let r=0;r<4;r++) state[c][r]^=ROUND_KEY[c][r];
phase=4; render('key','After AddRoundKey — XOR with round key (demo key, not real schedule)');
setLegend('The secret enters here. Without the key you cannot reverse any of the steps above.');
setButtons();
}
document.getElementById('btn-sub').addEventListener('click',doSubBytes);
document.getElementById('btn-shift').addEventListener('click',doShiftRows);
document.getElementById('btn-mix').addEventListener('click',doMixColumns);
document.getElementById('btn-key').addEventListener('click',doAddRoundKey);
document.getElementById('btn-reset').addEventListener('click',reset);
document.getElementById('msg').addEventListener('input',reset);
reset();
<div class="hint">Type 16 characters, then step through a single AES round.</div>
<div class="input-row">
<label for="msg">Plaintext (16 chars):</label>
<input id="msg" type="text" maxlength="16" value="Hello, AES World" autocomplete="off" spellcheck="false"/>
</div>
<div class="step-label" id="step-label">State (input bytes)</div>
<div id="grid" class="grid"></div>
<div class="legend" id="legend"></div>
<div class="btns">
<button id="btn-sub" type="button">SubBytes</button>
<button id="btn-shift" type="button">ShiftRows</button>
<button id="btn-mix" type="button">MixColumns</button>
<button id="btn-key" type="button">AddRoundKey</button>
<button id="btn-reset" type="button" class="ghost">Reset</button>
</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.hint { font-size: .85rem; color: #555; margin: 0 0 .6rem; }
.input-row { display: flex; align-items: center; gap: .5rem; margin-bottom: .7rem; flex-wrap: wrap; }
label { font-size: .9rem; }
input { font: 14px ui-monospace, monospace; padding: .3rem .5rem; border: 1px solid #adb1b8;
border-radius: 6px; width: 170px; }
.step-label { font-size: .8rem; font-weight: 700; letter-spacing: .06em; text-transform: uppercase;
color: #5a6a7a; margin-bottom: .35rem; }
.grid { display: grid; grid-template-columns: repeat(4, 52px); gap: 4px; margin-bottom: .6rem; }
.cell { width: 52px; height: 52px; display: flex; flex-direction: column; align-items: center;
justify-content: center; border-radius: 7px; font: 700 13px ui-monospace, monospace;
transition: background .35s, color .35s; user-select: none; }
.cell .hex { font-size: 14px; }
.cell .ch { font-size: 10px; color: inherit; opacity: .7; margin-top: 1px; }
.state-init { background: #e8eef3; color: #1d3557; border: 1px solid #cdd9e3; }
.state-sub { background: #fde8c0; color: #7a3f00; border: 1px solid #f4c77a; }
.state-shift { background: #d4f0e0; color: #0a4d28; border: 1px solid #74c99a; }
.state-mix { background: #dce8ff; color: #1a3584; border: 1px solid #7fa6f4; }
.state-key { background: #f0d4f0; color: #4d0a5e; border: 1px solid #c97fcc; }
.legend { font-size: .8rem; color: #555; margin-bottom: .5rem; min-height: 1.3em; }
.btns { display: flex; gap: .45rem; flex-wrap: wrap; }
button { font: 600 13px system-ui; padding: .4rem .85rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 7px; cursor: pointer; }
button:disabled { opacity: .38; cursor: default; }
button.ghost { background: #fff; color: #1d3557; }
button.ghost:disabled { opacity: .38; }
// AES S-box (FIPS 197, Figure 7)
const SBOX = [
0x63,0x7c,0x77,0x7b,0xf2,0x6b,0x6f,0xc5,0x30,0x01,0x67,0x2b,0xfe,0xd7,0xab,0x76,
0xca,0x82,0xc9,0x7d,0xfa,0x59,0x47,0xf0,0xad,0xd4,0xa2,0xaf,0x9c,0xa4,0x72,0xc0,
0xb7,0xfd,0x93,0x26,0x36,0x3f,0xf7,0xcc,0x34,0xa5,0xe5,0xf1,0x71,0xd8,0x31,0x15,
0x04,0xc7,0x23,0xc3,0x18,0x96,0x05,0x9a,0x07,0x12,0x80,0xe2,0xeb,0x27,0xb2,0x75,
0x09,0x83,0x2c,0x1a,0x1b,0x6e,0x5a,0xa0,0x52,0x3b,0xd6,0xb3,0x29,0xe3,0x2f,0x84,
0x53,0xd1,0x00,0xed,0x20,0xfc,0xb1,0x5b,0x6a,0xcb,0xbe,0x39,0x4a,0x4c,0x58,0xcf,
0xd0,0xef,0xaa,0xfb,0x43,0x4d,0x33,0x85,0x45,0xf9,0x02,0x7f,0x50,0x3c,0x9f,0xa8,
0x51,0xa3,0x40,0x8f,0x92,0x9d,0x38,0xf5,0xbc,0xb6,0xda,0x21,0x10,0xff,0xf3,0xd2,
0xcd,0x0c,0x13,0xec,0x5f,0x97,0x44,0x17,0xc4,0xa7,0x7e,0x3d,0x64,0x5d,0x19,0x73,
0x60,0x81,0x4f,0xdc,0x22,0x2a,0x90,0x88,0x46,0xee,0xb8,0x14,0xde,0x5e,0x0b,0xdb,
0xe0,0x32,0x3a,0x0a,0x49,0x06,0x24,0x5c,0xc2,0xd3,0xac,0x62,0x91,0x95,0xe4,0x79,
0xe7,0xc8,0x37,0x6d,0x8d,0xd5,0x4e,0xa9,0x6c,0x56,0xf4,0xea,0x65,0x7a,0xae,0x08,
0xba,0x78,0x25,0x2e,0x1c,0xa6,0xb4,0xc6,0xe8,0xdd,0x74,0x1f,0x4b,0xbd,0x8b,0x8a,
0x70,0x3e,0xb5,0x66,0x48,0x03,0xf6,0x0e,0x61,0x35,0x57,0xb9,0x86,0xc1,0x1d,0x9e,
0xe1,0xf8,0x98,0x11,0x69,0xd9,0x8e,0x94,0x9b,0x1e,0x87,0xe9,0xce,0x55,0x28,0xdf,
0x8c,0xa1,0x89,0x0d,0xbf,0xe6,0x42,0x68,0x41,0x99,0x2d,0x0f,0xb0,0x54,0xbb,0x16
];
// AES state is column-major (bytes fill down columns first).
// We display row-major (left to right, top to bottom) for readability.
// state[col][row] = byte.
// Fixed round key (demo only — not a real key schedule)
const ROUND_KEY = [
[0x2b,0x28,0xab,0x09],[0x7e,0xae,0xf7,0xcf],
[0x15,0xd2,0x15,0x4f],[0x16,0xa6,0x88,0x3c]
];
// GF(2^8) multiply by 2 (xtime)
function xtime(b){ return ((b<<1)^(b&0x80?0x1b:0))&0xff; }
// GF multiply
function gmul(a,b){
let p=0; for(let i=0;i<8;i++){
if(b&1) p^=a;
const hi=a&0x80; a=(a<<1)&0xff; if(hi) a^=0x1b; b>>=1;
} return p;
}
// Convert message string -> 4x4 state (column-major: state[col][row])
function msgToState(msg){
const bytes=[]; for(let i=0;i<16;i++) bytes.push(msg.charCodeAt(i)||0x20);
// Fill column-major
const s=[[],[],[],[]];
for(let col=0;col<4;col++) for(let row=0;row<4;row++) s[col][row]=bytes[row*4+col];
return s;
}
// Display helpers
function toHex(b){ return b.toString(16).padStart(2,'0').toUpperCase(); }
function toPrint(b){ return (b>=32&&b<127)?String.fromCharCode(b):'·'; }
let state=[], phase=0;
// phases: 0=init, 1=subbytes, 2=shiftrows, 3=mixcols, 4=addroundkey
function getInput(){
let m=document.getElementById('msg').value;
while(m.length<16) m+=' ';
return m.slice(0,16);
}
function deepCopy(s){ return s.map(col=>[...col]); }
function reset(){
phase=0;
state=msgToState(getInput());
render('init','State (input bytes) — column-major layout, row ↔ column swapped for display');
setButtons();
}
function renderGrid(s, cls){
const g=document.getElementById('grid'); g.innerHTML='';
// Display row-major: row r, col c -> byte s[c][r]
for(let r=0;r<4;r++) for(let c=0;c<4;c++){
const b=s[c][r];
const d=document.createElement('div');
d.className='cell '+cls;
d.innerHTML='<span class="hex">'+toHex(b)+'</span><span class="ch">'+toPrint(b)+'</span>';
g.appendChild(d);
}
}
function render(cls, label){
renderGrid(state, 'state-'+cls);
document.getElementById('step-label').textContent=label;
}
function setLegend(t){ document.getElementById('legend').textContent=t; }
function setButtons(){
const ids=['btn-sub','btn-shift','btn-mix','btn-key'];
// enable only the next step
ids.forEach((id,i)=>{ document.getElementById(id).disabled=(i!==phase); });
}
function doSubBytes(){
state=deepCopy(state);
for(let c=0;c<4;c++) for(let r=0;r<4;r++) state[c][r]=SBOX[state[c][r]];
phase=1; render('sub','After SubBytes — each byte replaced by S-box lookup');
setLegend('S-box maps every byte to a nonlinear substitute, breaking algebraic patterns.');
setButtons();
}
function doShiftRows(){
// Operate on rows: row r shifts left by r positions.
// Extract rows, shift, put back.
state=deepCopy(state);
for(let r=1;r<4;r++){
const row=[state[0][r],state[1][r],state[2][r],state[3][r]];
for(let c=0;c<4;c++) state[c][r]=row[(c+r)%4];
}
phase=2; render('shift','After ShiftRows — row 0 stays, row 1 shifts left 1, etc.');
setLegend('Bytes spread across all four columns — no column is left unchanged.');
setButtons();
}
function doMixColumns(){
state=deepCopy(state);
for(let c=0;c<4;c++){
const [s0,s1,s2,s3]=state[c];
state[c][0]=gmul(s0,2)^gmul(s1,3)^s2^s3;
state[c][1]=s0^gmul(s1,2)^gmul(s2,3)^s3;
state[c][2]=s0^s1^gmul(s2,2)^gmul(s3,3);
state[c][3]=gmul(s0,3)^s1^s2^gmul(s3,2);
}
phase=3; render('mix','After MixColumns — each column mixed via GF(2⁸) polynomial');
setLegend('One byte change in a column now affects all four bytes — full column diffusion.');
setButtons();
}
function doAddRoundKey(){
state=deepCopy(state);
for(let c=0;c<4;c++) for(let r=0;r<4;r++) state[c][r]^=ROUND_KEY[c][r];
phase=4; render('key','After AddRoundKey — XOR with round key (demo key, not real schedule)');
setLegend('The secret enters here. Without the key you cannot reverse any of the steps above.');
setButtons();
}
document.getElementById('btn-sub').addEventListener('click',doSubBytes);
document.getElementById('btn-shift').addEventListener('click',doShiftRows);
document.getElementById('btn-mix').addEventListener('click',doMixColumns);
document.getElementById('btn-key').addEventListener('click',doAddRoundKey);
document.getElementById('btn-reset').addEventListener('click',reset);
document.getElementById('msg').addEventListener('input',reset);
reset();
Notice how each step attacks the data differently. SubBytes changes every byte independently using a fixed lookup table (the S-box). ShiftRows slides the rows left by 0, 1, 2, or 3 positions, spreading bytes across columns. MixColumns blends the four bytes of each column with a polynomial in GF(28), so a change in one byte ripples into all four. AddRoundKey XORs the column round key in — the only step that uses the secret. After just one round, your original text is nearly unrecognizable.
Comments
Loading comments...