초보자를 위한 AES 알고리즘

발행: (2025년 12월 1일 오전 07:43 GMT+9)
9 min read
원문: Dev.to

Source: Dev.to

개요

AES는 대칭키 암호화 알고리즘으로, 매우 안전하고 구현이 쉬우며 실제 세계에서 사용됩니다.
이 가이드는 128‑bit CTR 모드 변형을 C로 구현하는 방법을 알려줍니다.

절차:

  1. 128‑bit 암호 키를 무작위로 생성합니다.
  2. 친구와 비밀히 공유합니다(예: Diffie‑Hellman – 여기서는 다루지 않음).
  3. 키로 메시지를 암호화합니다.
  4. 암호화된 메시지를 전송합니다.
  5. 친구가 동일한 키로 복호화합니다.

테스트 데이터는 YouTube에서 확인할 수 있습니다.


AES128 구현

핵심 함수는 16‑byte 블록 하나를 16‑byte 키로 암호화합니다:

void AES128( uint8_t* block, uint8_t* cipherKey) { }

이 함수는 블록에 순차적으로 작용하는 여러 보조 함수들의 파이프라인입니다.

AES overview

11개의 “Apply Key” 단계 각각은 getNthRoundKey를 통해 메인 암호 키에서 파생된 서로 다른 라운드 키를 사용합니다.

가이드 후반부에서는 AES128을 반복 호출하는 고수준 보조 함수들을 정의합니다:

uint8_t* encrypt(uint8_t* plainText, uint64_t lengthInBytes, uint8_t* cipherKey) { }
uint8_t* decrypt(uint8_t* encryptedText, uint64_t lengthInBytes, uint8_t* cipherKey) { }

블록

블록은 바이트(0‑255) 4 × 4 행렬입니다. C에서는 평탄한 배열 uint8_t[16](스택) 또는 uint8_t*(힙)으로 저장됩니다. 예시 블록: {0,1,2,…,15}.

4x4 block

참고: 블록은 열 우선(column‑major) 으로 저장됩니다(두 번째 요소는 첫 번째 요소 아래에 위치).

단계 1 – 블록 출력

void printBlock(uint8_t* block) { }

단계 2 – 요소 설정

void setElement(uint8_t* block, int nthRow, int nthCol, uint8_t element) { }

행과 열은 각각 4바이트 평탄 배열입니다. 행의 첫 번째 요소는 가장 왼쪽, 열의 첫 번째 요소는 가장 위에 있습니다.

단계 3 – 행 / 열 추출

uint8_t* getRow(uint8_t* block, int nthRow) {
    // malloc storage for a new row
    // copy values from block to the new storage
    // return the newly allocated storage
}

uint8_t* getCol(uint8_t* block, int nthCol) {
    // same as above for a column
}

단계 4 – 행 / 열 출력

void printRow(uint8_t* row) { }
void printCol(uint8_t* col) { }

중요: 메모리를 할당하는 함수(getRow, getCol)는 사용 후 반환된 포인터를 반드시 해제해야 합니다.

단계 5 – 회전 연산

Rotate illustration

void rotateLeftRow(uint8_t* row) { }
void rotateUpCol(uint8_t* col) { }
// Operate in‑place; do not allocate new memory.

단계 6 – 행 / 열을 블록에 다시 쓰기

void setRow(uint8_t* block, uint8_t* row, int nthRow) {
    // copy row into the block (no allocations)
}
void setCol(uint8_t* block, uint8_t* col, int nthCol) {
    // copy column into the block (no allocations)
}

이 기본 연산들은 AES‑128 파이프라인의 토대가 됩니다; 진행하기 전에 반드시 검증하세요.


subBytes 함수

S‑box 상수(인덱스 0 → 255)는 다음과 같습니다:

{99,124,119,123,242,107,111,197,48,1,103,43,254,215,171,118,
202,130,201,125,250,89,71,240,173,212,162,175,156,164,114,192,
183,253,147,38,54,63,247,204,52,165,229,241,113,216,49,21,
4,199,35,195,24,150,5,154,7,18,128,226,235,39,178,117,
9,131,44,26,27,110,90,160,82,59,214,179,41,227,47,132,
83,209,0,237,32,252,177,91,106,203,190,57,74,76,88,207,
208,239,170,251,67,77,51,133,69,249,2,127,80,60,159,168,
81,163,64,143,146,157,56,245,188,182,218,33,16,255,243,210,
205,12,19,236,95,151,68,23,196,167,126,61,100,93,25,115,
96,129,79,220,34,42,144,136,70,238,184,20,222,94,11,219,
224,50,58,10,73,6,36,92,194,211,172,98,145,149,228,121,
231,200,55,109,141,213,78,169,108,86,244,234,101,122,174,8,
186,120,37,46,28,166,180,198,232,221,116,31,75,189,139,138,
112,62,181,102,72,3,246,14,97,53,87,185,134,193,29,158,
225,248,152,17,105,217,142,148,155,30,135,233,206,85,40,223,
140,161,137,13,191,230,66,104,65,153,45,15,176,84,187,22}

단계 7 – S‑box 접근 함수

uint8_t sbox(uint8_t i) { }

단계 8 – 상태에 S‑box 적용

void subBytes(uint8_t* state) {
    // replace each byte with sbox(byte)
    // operate in‑place, no allocations
}

shiftRows 함수

ShiftRows diagram

단계 9 – shiftRows 구현

void shiftRows(uint8_t* state) {
    // For each row:
    //   1. getRow()
    //   2. rotateLeftRow() the appropriate number of positions
    //   3. setRow()
    // Free any allocated rows.
}

mixColumns 함수

먼저 Galois‑field 곱셈 보조 함수가 필요합니다. 미리 계산된 테이블이 제공됩니다.

테이블

  • b = 1 테이블 – 0‑255 순서대로 값이 나열됩니다.
  • b = 2 테이블 – 원본 내용 참고.
  • b = 3 테이블 – 원본 내용 참고.

단계 10 – gfmul

uint8_t gfmul(uint8_t a, uint8_t b) {
    // a ∈ [0,255], b ∈ {1,2,3}
    // return the corresponding entry from the appropriate table
}

단계 11 – mixColumns

MixColumns diagram

void mixColumns(uint8_t* state) {
    // 1. Allocate a temporary 4×4 block for the output.
    // 2. For each output element (row r, col c):
    //      - fetch row r of the multiplication matrix,
    //      - fetch column c of the input state,
    //      - compute Σ gfmul(matrix_elem, state_elem) (xor the results),
    //      - store in the temporary block.
    // 3. Copy the temporary block back into `state`.
    // 4. Free the temporary block.
}

useRoundKey 함수

단계 12 – 라운드 키와 XOR

void useRoundKey(uint8_t* state, uint8_t* roundKey) {
    // for i = 0..15: state[i] ^= roundKey[i];
}

키 확장

11개의 라운드 키는 원본 암호 키에서 파생된 4 × 4 블록입니다.

Key expansion overview

열은 w0, w1, …, w43 로 표시됩니다.
라운드 키 0은 암호 키를 그대로 복사한 것입니다.

단계 13 – 라운드 키의 열 가져오기

uint8_t* getNthColOfRoundKey(int nthCol, uint8_t* cipherKey) {
    uint8_t rcon[] = {1,2,4,8,16,32,64,128,27,54};

    if (/* nthCol is 0‑3 */) {
        // return the corresponding column from cipherKey using getCol()
    } else if (/* nthCol ≡ 1 (mod 4) */) {
        // allocate new column
        // col = getNthColOfRoundKey(nthCol-1) XOR getNthColOfRoundKey(nthCol-4)
        // return col
    } else {
        // allocate new column
        // tmp = getNthColOfRoundKey(nthCol-1)
        // rotateUpCol(tmp)
        // col = tmp XOR getNthColOfRoundKey(nthCol-4)
        // col[0] ^= rcon[nthCol/4 - 1]; // apply rcon
        // return col
    }
}

단계 14 – 전체 라운드 키 가져오기

uint8_t* getNthRoundKey(int n, uint8_t* cipherKey) {
    // allocate 16‑byte block
    // for each column i = 0..3:
    //     copy getNthColOfRoundKey(4*n + i) into the block
    // return the block
}

단계 15 – AES128 완성

void AES128(uint8_t* block, uint8_t* cipherKey) {
    // 1. Initial AddRoundKey (round 0)
    // 2. For rounds 1‑9:
    //      subBytes()
    //      shiftRows()
    //      mixColumns()
    //      useRoundKey() with roundKey = getNthRoundKey(round, cipherKey)
    // 3. Final round (10):
    //      subBytes()
    //      shiftRows()
    //      useRoundKey() with roundKey = getNthRoundKey(10, cipherKey)
    // Free any allocated round keys.
}

암호 키를 키스트림으로 변환 (CTR 모드)

CTR mode diagram

암호화 단계:

  1. 무작위 64‑bit nonce를 생성합니다.
  2. 평문을 16‑byte 블록으로 나눕니다.
  3. 각 블록 i에 대해 16‑byte 키스트림을 구성합니다:
    • 앞 8 byte = nonce
    • 뒤 8 byte = i (8‑byte 리틀 엔디안)
    • 이 키스트림에 AES128을 암호 키와 함께 실행합니다.
  4. 키스트림과 평문 블록을 XOR합니다.
  5. nonce를 암호문 뒤에 붙입니다.

복호화는 동일한 과정을 역순으로 수행하며, 마지막 8 byte에서 nonce를 추출합니다.

단계 16 – 64‑bit 숫자에서 바이트 추출

uint8_t getNthByte(uint64_t number, int n) {
    // returns the n‑th least‑significant byte (0‑based)
    return (number >> (n * 8)) & 0xFF;
}

단계 17 – 블록용 키스트림 생성

uint8_t* getKeystream(uint8_t* cipherKey, uint64_t nthBlock, uint8_t* nonce) {
    // allocate 16‑byte array `keystream`
    // copy nonce (8 bytes) into keystream[0..7]
    // convert nthBlock to 8‑byte array and copy into keystream[8..15]
    // AES128(keystream, cipherKey);
    // return keystream;
}

단계 18 – 메시지 암호화

uint8_t* encrypt(uint8_t* plainText, uint64_t lengthInBytes, uint8_t* cipherKey) {
    // 1. allocate 8‑byte nonce and fill with random data
    // 2. allocate output buffer of size lengthInBytes + 8 (nonce)
    // 3. for each 16‑byte block:
    //        keystream = getKeystream(cipherKey, blockIndex, nonce);
    //        xor plaintext block with keystream → ciphertext block
    //        free keystream
    // 4. copy nonce to the last 8 bytes of the output
    // 5. return the output buffer
}

단계 19 – 메시지 복호화

uint8_t* decrypt(uint8_t* encryptedText, uint64_t lengthInBytes, uint8_t* cipherKey) {
    // 1. allocate 8‑byte nonce
    // 2. copy the last 8 bytes of encryptedText into nonce
    // 3. allocate output buffer of size lengthInBytes - 8
    // 4. for each 16‑byte block:
    //        keystream = getKeystream(cipherKey, blockIndex, nonce);
    //        xor ciphertext block with keystream → plaintext block
    //        free keystream
    // 5. return the plaintext buffer
}

단계 20 – (선택) 파일 기반 유틸리티

프로그램을 수정하여 평문 파일을 읽고, nonce를 포함한 암호화된 출력을 파일에 쓰는 기능과, 복호화를 위한 반대 작업을 구현합니다.

Back to Blog

관련 글

더 보기 »

창고 활용에 대한 종합 가이드

소개 창고는 근본적으로 3‑D 박스일 뿐입니다. Utilisation은 실제로 그 박스를 얼마나 사용하고 있는지를 측정하는 지표입니다. While logistics c...