В современном мире защита данных становится критически важной. Многие известные алгоритмы шифрования (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: %sn", *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: %sn", *outPtr)
} else {
fmt.Println(string(plaintext))
}
default:
fmt.Printf("Unknown command: %sn", 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-приложения с поддержкой файлов. Такой проект позволяет не только лучше понять принципы криптографии, но и применить полученные знания на практике.
Важно помнить, что собственные алгоритмы шифрования в образовательных целях – отличный эксперимент, однако для реальной защиты информации рекомендуется использовать проверенные решения, прошедшие независимый аудит. Надеюсь, данный материал послужит хорошей отправной точкой для ваших исследований в области криптографии и вдохновит на создание новых, интересных проектов.
Автор: ImranAkhmedov