Как стать автором
Обновить

Как создать свой алгоритм шифрования: от идеи до готового CLI-приложения

Уровень сложностиСредний
Время на прочтение10 мин
Количество просмотров666

В современном мире защита данных становится критически важной. Многие известные алгоритмы шифрования (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)):

  1. Инициализация суммы ( \text{sum} = 0 ).

  2. В каждом раунде вычисляется временная переменная: [ T = \left( \left( (R \ll 4) \oplus (R \gg 5) \right) + R \right) \oplus (\text{sum} + K[\text{индекс}]) ] где (K) – массив из 4-х 32-битных слов, полученных из 128-битного ключа, а индекс выбирается на основе значения суммы.

  3. Левая половина (L) обновляется по формуле: [ L = (L + T) \mod 2^{32} ]

  4. Сумма увеличивается на константу (\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-приложения с поддержкой файлов. Такой проект позволяет не только лучше понять принципы криптографии, но и применить полученные знания на практике.

Важно помнить, что собственные алгоритмы шифрования в образовательных целях – отличный эксперимент, однако для реальной защиты информации рекомендуется использовать проверенные решения, прошедшие независимый аудит. Надеюсь, данный материал послужит хорошей отправной точкой для ваших исследований в области криптографии и вдохновит на создание новых, интересных проектов.

Теги:
Хабы:
+3
Комментарии16

Публикации

Истории

Работа

Ближайшие события

25 – 26 апреля
IT-конференция Merge Tatarstan 2025
Казань