В современном мире защита данных становится критически важной. Многие известные алгоритмы шифрования (AES, RSA, Blowfish) прошли долгий путь испытаний временем и экспертной оценкой. Однако создание собственного алгоритма шифрования – это отличный способ углубиться в мир криптографии, лучше понять принципы защиты информации и научиться реализовывать криптографические конструкции на практике.
В этой статье мы подробно разберем этапы разработки алгоритма шифрования, от концепции до реализации на языке Go. Мы пройдем путь от математической схемы до конечного CLI-приложения, способного работать с текстовыми данными и файлами.
Глава 1. Разработка алгоритма
1.1 Постановка задачи
Перед тем как приступать к реализации, необходимо определить основные цели алгоритма:
Тип шифрования. Мы будем использовать симметричный алгоритм, где один и тот же ключ применяется для шифрования и дешифрования.
Безопасность. Алгоритм должен быть устойчив к известным видам криптоанализа, включая атаки по открытому тексту и анализ повторяющихся блоков.
Эффективность. Алгоритм должен корректно работать как с короткими сообщениями, так и с большими файлами, не требуя чрезмерных вычислительных ресурсов.
Режим работы. Мы будем применять блочное шифрование в режиме CBC (Cipher Block Chaining), чтобы обеспечить случайность зашифрованных данных за счет использования инициализационного вектора (IV).
Дополнительные меры. Для правильной работы с данными используется паддинг по стандарту PKCS#7, что позволяет корректно обрабатывать данные, длина которых не кратна размеру блока.
1.2 Архитектура алгоритма
Мы выбрали модифицированную схему, основанную на идеях алгоритма XTEA. Основная идея заключается в следующем:
Разбиение ключа. Для повышения стойкости алгоритма входной симметричный ключ длиной 256 бит делится на две части по 128 бит. Каждая часть используется для последовательного шифрования каждого блока.
Сеть Фейстеля. Блок данных (64 бита) разбивается на две 32-битные части, которые обрабатываются через серию раундов с применением нелинейных преобразований. Многократное применение таких преобразований создает эффект лавинного эффекта, при котором изменение одного бита исходного текста приводит к значительным изменениям в шифротексте.
Режим CBC. Перед шифрованием каждый блок данных смешивается (операция XOR) с предыдущим зашифрованным блоком. Для первого блока используется случайно сгенерированный IV. Это препятствует повторению идентичных блоков шифротекста при шифровании похожих данных.
1.3 Математическая модель
В основе алгоритма лежит простая схема, состоящая из 32 раундов обработки. Рассмотрим основные операции для одного блока (разбитого на две половины (L) и (R)):
Инициализация суммы ( \text{sum} = 0 ).
В каждом раунде вычисляется временная переменная: [ T = \left( \left( (R \ll 4) \oplus (R \gg 5) \right) + R \right) \oplus (\text{sum} + K[\text{индекс}]) ] где (K) – массив из 4-х 32-битных слов, полученных из 128-битного ключа, а индекс выбирается на основе значения суммы.
Левая половина (L) обновляется по формуле: [ L = (L + T) \mod 2^{32} ]
Сумма увеличивается на константу (\delta) (например, (\delta = 0x9E3779B9)), и затем выполняется аналогичная операция для правой половины (R).
Эта процедура повторяется 32 раза, что обеспечивает достаточную нелинейность и диффузию. Для повышения стойкости алгоритма каждый блок шифруется дважды – сначала с использованием первой половины ключа, затем – со второй.
Глава 2. Реализация алгоритма в виде функций
2.1 Функции шифрования и дешифрования блока
Первый этап реализации – создание функций для шифрования и дешифрования одного 64-битного блока. Эти функции используют битовые операции (сдвиги, XOR, сложение с переполнением) для имитации работы с 32-битными регистрами.
Пример функции шифрования блока выглядит следующим образом:
func xteaEncryptBlock(L, R uint32, keyParts []uint32) (uint32, uint32) {
var sum uint32 = 0
for i := 0; i < 32; i++ {
T := (((R << 4) ^ (R >> 5)) + R) ^ (sum + keyParts[sum&3])
L += T
sum += delta
T = (((L << 4) ^ (L >> 5)) + L) ^ (sum + keyParts[(sum>>11)&3])
R += T
}
return L, R
}
Функция дешифрования практически зеркально отражает процесс шифрования, начиная с начального значения суммы, равного ( \delta \times 32 ) (с учетом модульного переполнения):
func xteaDecryptBlock(L, R uint32, keyParts []uint32) (uint32, uint32) {
sum := uint32((uint64(delta) * 32) % 0x100000000)
for i := 0; i < 32; i++ {
T := (((L << 4) ^ (L >> 5)) + L) ^ (sum + keyParts[(sum>>11)&3])
R -= T
sum -= delta
T = (((R << 4) ^ (R >> 5)) + R) ^ (sum + keyParts[sum&3])
L -= T
}
return L, R
}
2.2 Функции для обработки данных
Для работы с произвольными сообщениями необходимо обеспечить корректное дополнение (padding) данных до кратности размеру блока. Стандарт PKCS#7 широко используется для этой цели:
func PKCS7Padding(data []byte, blockSize int) []byte {
padLen := blockSize - (len(data) % blockSize)
if padLen == 0 {
padLen = blockSize
}
padding := bytes.Repeat([]byte{byte(padLen)}, padLen)
return append(data, padding...)
}
func PKCS7Unpadding(data []byte) ([]byte, error) {
if len(data) == 0 {
return nil, fmt.Errorf("empty data")
}
padLen := int(data[len(data)-1])
if padLen == 0 || padLen > len(data) {
return nil, fmt.Errorf("invalid padding")
}
for i := len(data) - padLen; i < len(data); i++ {
if int(data[i]) != padLen {
return nil, fmt.Errorf("invalid padding")
}
}
return data[:len(data)-padLen], nil
}
2.3 Реализация режима CBC
Чтобы обеспечить случайность зашифрованного текста, мы используем режим CBC (Cipher Block Chaining). При шифровании каждый блок сначала смешивается с предыдущим зашифрованным блоком (или с IV для первого блока), а затем шифруется.
Функция шифрования данных с использованием CBC выглядит следующим образом:
func EncryptData(plaintext, key []byte) ([]byte, error) {
if len(key) != 32 {
return nil, fmt.Errorf("key must be 256-bit (32 bytes)")
}
keyParts := make([]uint32, 8)
for i := 0; i < 8; i++ {
keyParts[i] = binary.LittleEndian.Uint32(key[i*4 : (i+1)*4])
}
key1 := keyParts[:4]
key2 := keyParts[4:]
iv := make([]byte, blockSize)
if _, err := rand.Read(iv); err != nil {
return nil, err
}
padded := PKCS7Padding(plaintext, blockSize)
ciphertext := make([]byte, 0, len(iv)+len(padded))
ciphertext = append(ciphertext, iv...)
prevBlock := iv
for i := 0; i < len(padded); i += blockSize {
block := padded[i : i+blockSize]
x := make([]byte, blockSize)
for j := 0; j < blockSize; j++ {
x[j] = block[j] ^ prevBlock[j]
}
L := binary.LittleEndian.Uint32(x[0:4])
R := binary.LittleEndian.Uint32(x[4:8])
L, R = xteaEncryptBlock(L, R, key1)
L, R = xteaEncryptBlock(L, R, key2)
outBlock := make([]byte, blockSize)
binary.LittleEndian.PutUint32(outBlock[0:4], L)
binary.LittleEndian.PutUint32(outBlock[4:8], R)
ciphertext = append(ciphertext, outBlock...)
prevBlock = outBlock
}
return ciphertext, nil
}
Функция дешифрования обратна шифрованию и восстанавливает исходное сообщение:
func DecryptData(ciphertext, key []byte) ([]byte, error) {
if len(key) != 32 {
return nil, fmt.Errorf("key must be 256-bit (32 bytes)")
}
if len(ciphertext) < blockSize {
return nil, fmt.Errorf("ciphertext too short")
}
keyParts := make([]uint32, 8)
for i := 0; i < 8; i++ {
keyParts[i] = binary.LittleEndian.Uint32(key[i*4 : (i+1)*4])
}
key1 := keyParts[:4]
key2 := keyParts[4:]
iv := ciphertext[:blockSize]
encryptedBlocks := ciphertext[blockSize:]
if len(encryptedBlocks)%blockSize != 0 {
return nil, fmt.Errorf("ciphertext is not a multiple of block size")
}
prevBlock := iv
plaintextPadded := make([]byte, 0, len(encryptedBlocks))
for i := 0; i < len(encryptedBlocks); i += blockSize {
cipherBlock := encryptedBlocks[i : i+blockSize]
L := binary.LittleEndian.Uint32(cipherBlock[0:4])
R := binary.LittleEndian.Uint32(cipherBlock[4:8])
L, R = xteaDecryptBlock(L, R, key2)
L, R = xteaDecryptBlock(L, R, key1)
x := make([]byte, blockSize)
binary.LittleEndian.PutUint32(x[0:4], L)
binary.LittleEndian.PutUint32(x[4:8], R)
plainBlock := make([]byte, blockSize)
for j := 0; j < blockSize; j++ {
plainBlock[j] = x[j] ^ prevBlock[j]
}
plaintextPadded = append(plaintextPadded, plainBlock...)
prevBlock = cipherBlock
}
return PKCS7Unpadding(plaintextPadded)
}
Глава 3. Конечный код с поддержкой работы с файлами и CLI
На заключительном этапе мы объединяем все функции в единое CLI-приложение. Программа принимает параметры командной строки (субкоманды encrypt
и decrypt
) и позволяет работать как с текстовыми данными, так и с файлами. Пользователь может задавать ключ, входные данные и путь для сохранения результата.
3.1 Структура CLI-приложения
Приложение использует стандартный пакет flag
для обработки параметров:
--key: строка-ключ, которая затем преобразуется в 256-битный ключ через SHA‑256.
--input: входной текст для шифрования или дешифрования.
--file: путь к входному файлу (приоритет выше, чем --input).
--out: путь для сохранения результата (если не указан, результат выводится в консоль).
3.2 Итоговый код
Ниже представлен полный исходный код приложения:
package main
import (
"bytes"
"crypto/rand"
"crypto/sha256"
"encoding/binary"
"encoding/hex"
"flag"
"fmt"
"io/ioutil"
"log"
"os"
"strings"
)
const blockSize = 8
const delta uint32 = 0x9E3779B9
func xteaEncryptBlock(L, R uint32, keyParts []uint32) (uint32, uint32) {
var sum uint32 = 0
for i := 0; i < 32; i++ {
T := (((R << 4) ^ (R >> 5)) + R) ^ (sum + keyParts[sum&3])
L += T
sum += delta
T = (((L << 4) ^ (L >> 5)) + L) ^ (sum + keyParts[(sum>>11)&3])
R += T
}
return L, R
}
func xteaDecryptBlock(L, R uint32, keyParts []uint32) (uint32, uint32) {
sum := uint32((uint64(delta) * 32) % 0x100000000)
for i := 0; i < 32; i++ {
T := (((L << 4) ^ (L >> 5)) + L) ^ (sum + keyParts[(sum>>11)&3])
R -= T
sum -= delta
T = (((R << 4) ^ (R >> 5)) + R) ^ (sum + keyParts[sum&3])
L -= T
}
return L, R
}
func PKCS7Padding(data []byte, blockSize int) []byte {
padLen := blockSize - (len(data) % blockSize)
if padLen == 0 {
padLen = blockSize
}
padding := bytes.Repeat([]byte{byte(padLen)}, padLen)
return append(data, padding...)
}
func PKCS7Unpadding(data []byte) ([]byte, error) {
if len(data) == 0 {
return nil, fmt.Errorf("empty data")
}
padLen := int(data[len(data)-1])
if padLen == 0 || padLen > len(data) {
return nil, fmt.Errorf("invalid padding")
}
for i := len(data) - padLen; i < len(data); i++ {
if int(data[i]) != padLen {
return nil, fmt.Errorf("invalid padding")
}
}
return data[:len(data)-padLen], nil
}
func EncryptData(plaintext, key []byte) ([]byte, error) {
if len(key) != 32 {
return nil, fmt.Errorf("key must be 256-bit (32 bytes)")
}
keyParts := make([]uint32, 8)
for i := 0; i < 8; i++ {
keyParts[i] = binary.LittleEndian.Uint32(key[i*4 : (i+1)*4])
}
key1 := keyParts[:4]
key2 := keyParts[4:]
iv := make([]byte, blockSize)
if _, err := rand.Read(iv); err != nil {
return nil, err
}
padded := PKCS7Padding(plaintext, blockSize)
ciphertext := make([]byte, 0, len(iv)+len(padded))
ciphertext = append(ciphertext, iv...)
prevBlock := iv
for i := 0; i < len(padded); i += blockSize {
block := padded[i : i+blockSize]
x := make([]byte, blockSize)
for j := 0; j < blockSize; j++ {
x[j] = block[j] ^ prevBlock[j]
}
L := binary.LittleEndian.Uint32(x[0:4])
R := binary.LittleEndian.Uint32(x[4:8])
L, R = xteaEncryptBlock(L, R, key1)
L, R = xteaEncryptBlock(L, R, key2)
outBlock := make([]byte, blockSize)
binary.LittleEndian.PutUint32(outBlock[0:4], L)
binary.LittleEndian.PutUint32(outBlock[4:8], R)
ciphertext = append(ciphertext, outBlock...)
prevBlock = outBlock
}
return ciphertext, nil
}
func DecryptData(ciphertext, key []byte) ([]byte, error) {
if len(key) != 32 {
return nil, fmt.Errorf("key must be 256-bit (32 bytes)")
}
if len(ciphertext) < blockSize {
return nil, fmt.Errorf("ciphertext too short")
}
keyParts := make([]uint32, 8)
for i := 0; i < 8; i++ {
keyParts[i] = binary.LittleEndian.Uint32(key[i*4 : (i+1)*4])
}
key1 := keyParts[:4]
key2 := keyParts[4:]
iv := ciphertext[:blockSize]
encryptedBlocks := ciphertext[blockSize:]
if len(encryptedBlocks)%blockSize != 0 {
return nil, fmt.Errorf("ciphertext is not a multiple of block size")
}
prevBlock := iv
plaintextPadded := make([]byte, 0, len(encryptedBlocks))
for i := 0; i < len(encryptedBlocks); i += blockSize {
cipherBlock := encryptedBlocks[i : i+blockSize]
L := binary.LittleEndian.Uint32(cipherBlock[0:4])
R := binary.LittleEndian.Uint32(cipherBlock[4:8])
L, R = xteaDecryptBlock(L, R, key2)
L, R = xteaDecryptBlock(L, R, key1)
x := make([]byte, blockSize)
binary.LittleEndian.PutUint32(x[0:4], L)
binary.LittleEndian.PutUint32(x[4:8], R)
plainBlock := make([]byte, blockSize)
for j := 0; j < blockSize; j++ {
plainBlock[j] = x[j] ^ prevBlock[j]
}
plaintextPadded = append(plaintextPadded, plainBlock...)
prevBlock = cipherBlock
}
return PKCS7Unpadding(plaintextPadded)
}
func main() {
if len(os.Args) < 2 {
fmt.Println("Usage:")
fmt.Println(" encrypt --key 'example' [--input 'text'] [--file <file>] [--out <output_file>]")
fmt.Println(" decrypt --key 'example' [--input 'hex-text'] [--file <file>] [--out <output_file>]")
os.Exit(1)
}
cmd := os.Args[1]
switch strings.ToLower(cmd) {
case "encrypt":
encryptCmd := flag.NewFlagSet("encrypt", flag.ExitOnError)
keyPtr := encryptCmd.String("key", "", "Encryption key")
inputPtr := encryptCmd.String("input", "", "Input text to encrypt")
filePtr := encryptCmd.String("file", "", "Input file path (overrides --input)")
outPtr := encryptCmd.String("out", "", "Output file path")
encryptCmd.Parse(os.Args[2:])
if *keyPtr == "" {
log.Fatal("Key is required via --key")
}
hashedKey := sha256.Sum256([]byte(*keyPtr))
var inputData []byte
var err error
if *filePtr != "" {
inputData, err = ioutil.ReadFile(*filePtr)
if err != nil {
log.Fatalf("Error reading file %s: %v", *filePtr, err)
}
} else if *inputPtr != "" {
inputData = []byte(*inputPtr)
} else {
log.Fatal("Input data must be provided via --input or --file")
}
ciphertext, err := EncryptData(inputData, hashedKey[:])
if err != nil {
log.Fatalf("Error encrypting: %v", err)
}
output := hex.EncodeToString(ciphertext)
if *outPtr != "" {
err = ioutil.WriteFile(*outPtr, []byte(output), 0644)
if err != nil {
log.Fatalf("Error writing to file %s: %v", *outPtr, err)
}
fmt.Printf("Result written to file: %s\n", *outPtr)
} else {
fmt.Println(output)
}
case "decrypt":
decryptCmd := flag.NewFlagSet("decrypt", flag.ExitOnError)
keyPtr := decryptCmd.String("key", "", "Decryption key")
inputPtr := decryptCmd.String("input", "", "Input hex-text to decrypt")
filePtr := decryptCmd.String("file", "", "Input file path (overrides --input)")
outPtr := decryptCmd.String("out", "", "Output file path")
decryptCmd.Parse(os.Args[2:])
if *keyPtr == "" {
log.Fatal("Key is required via --key")
}
hashedKey := sha256.Sum256([]byte(*keyPtr))
var inputData []byte
var err error
if *filePtr != "" {
inputData, err = ioutil.ReadFile(*filePtr)
if err != nil {
log.Fatalf("Error reading file %s: %v", *filePtr, err)
}
} else if *inputPtr != "" {
inputData = []byte(*inputPtr)
} else {
log.Fatal("Input data must be provided via --input or --file")
}
cipherBytes, err := hex.DecodeString(strings.TrimSpace(string(inputData)))
if err != nil {
log.Fatalf("Error decoding hex: %v", err)
}
plaintext, err := DecryptData(cipherBytes, hashedKey[:])
if err != nil {
log.Fatalf("Error decrypting: %v", err)
}
if *outPtr != "" {
err = ioutil.WriteFile(*outPtr, plaintext, 0644)
if err != nil {
log.Fatalf("Error writing to file %s: %v", *outPtr, err)
}
fmt.Printf("Result written to file: %s\n", *outPtr)
} else {
fmt.Println(string(plaintext))
}
default:
fmt.Printf("Unknown command: %s\n", cmd)
fmt.Println("Usage:")
fmt.Println(" encrypt --key 'example' [--input 'text'] [--file <file>] [--out <output_file>]")
fmt.Println(" decrypt --key 'example' [--input 'hex-text'] [--file <file>] [--out <output_file>]")
os.Exit(1)
}
}
Заключение
В этой статье мы подробно разобрали, как разработать собственный алгоритм шифрования, начиная с определения задачи и математической модели, затем перейдя к реализации базовых функций (шифрование блока, паддинг, режим CBC), и завершили создание полноценного CLI-приложения с поддержкой файлов. Такой проект позволяет не только лучше понять принципы криптографии, но и применить полученные знания на практике.
Важно помнить, что собственные алгоритмы шифрования в образовательных целях – отличный эксперимент, однако для реальной защиты информации рекомендуется использовать проверенные решения, прошедшие независимый аудит. Надеюсь, данный материал послужит хорошей отправной точкой для ваших исследований в области криптографии и вдохновит на создание новых, интересных проектов.