Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!
Современные стандарты разработки пользовательских приложений выдвигают определенные требования к шифрованию информации. Например, документ RFC-7539 содержит подробную и исчерпывающую информацию о том, какие алгоритмы рекомендуется применять и как программировать некоторые из них. Далее предлагается к подробному рассмотрению один из этих алгоритмов - ChaCha20.
Разработка
Сначала нужно определить типы входных и выходных данных. Так как речь идет о шифровании данных, то вся работа заключается в манипулировании битами. Не имеет значение, что нужно шифровать, будь то текст, изображение или видео. Каждый из этих типов возможно преобразовать в набор битов и байтов, после чего применять конкретный алгоритм. Таким образом, основными типами, с которыми работает библиотека это байт - UInt8 (Byte) и последовательность из четырех байтов - UInt32 (Word).
typealias Byte = UInt8
final class Word {
// MARK: - Types
typealias Value = UInt32
// MARK: - Properties
var value: Value
// MARK: - Lifecycle
init(_ value: Value) {
self.value = value
}
init(bytes: [Byte]) throws {
value = 0
value = try getValue(from: bytes)
}
// MARK: - Methods
func getBytes() -> [Byte] {
var bytes: [Byte] = []
for shift in stride(from: 24, through: 0, by: -8) {
let byte = Byte(truncatingIfNeeded: value.bigEndian >> shift)
bytes.append(byte)
}
return bytes
}
private func getValue(from bytes: [Byte]) throws -> Value {
guard bytes.count == 4 else {
throw CryptoError.invalidSize
}
var value: Value = 0
let chunk = bytes[0..<bytes.count]
for (index, byte) in chunk.reversed().enumerated() {
var partition = Value(byte)
partition <<= byte.bitWidth * index
value |= partition.bigEndian
}
return value
}
}
Определив типы данных, нужно инициализировать начальное состояние, используя ключ подписи и так называемый nonce. Здесь все предельно понятно, типы данных преобразуются к массиву байт и задают начальное состояние объекта.
final class ChaCha20 {
// MARK: - Properties
private var state: [Word]
// MARK: - Lifecycle
init(_ key: [Byte], _ nonce: [Byte]) throws {
state = [Word](repeating: Word(0), count: 16)
state[0] = Word(0x61707865)
state[1] = Word(0x3320646e)
state[2] = Word(0x79622d32)
state[3] = Word(0x6b206574)
let keyWordsIndexes = 4...11
let nonceWordsIndexes = 13...15
for index in keyWordsIndexes {
let offset = (index - keyWordsIndexes.lowerBound) * 4
let bytes = key[offset..<offset + 4]
state[index] = try Word(bytes: Array(bytes))
}
for index in nonceWordsIndexes {
let offset = (index - nonceWordsIndexes.lowerBound) * 4
let bytes = nonce[offset..<offset + 4]
state[index] = try Word(bytes: Array(bytes))
}
}
}
RFC-7539 секция 2.3. Рекомендации на тему того, какие значения нужно указывать в качестве констант, однако у разработчика есть возможность задать собственные величины. Например, первые четыре значения state, а также значение счетчика операций state[12] (будет показано ниже) и количество итераций выполнения цикла формирования битовой маски (тоже будет показано ниже).
RFC-7539 секция 2.1. Главным методом для осуществления криптографического шифрования является quarterRound(_:_:_:_:). С его помощью над четырьмя числами проводится операции по изменению значений их битов.
Забегая наперед, нужно сказать, что ключевой операцией здесь является исключающее или (XOR). Именно по этой причине алгоритм достаточно выполнить один раз для шифрования и еще один раз для дешифрования.
private func quarterRound(_ a: Word, _ b: Word, _ c: Word, _ d: Word) {
a.value &+= b.value
d.value ^= a.value
d.value <<<= 16
c.value &+= d.value
b.value ^= c.value
b.value <<<= 12
a.value &+= b.value
d.value ^= a.value
d.value <<<= 8
c.value &+= d.value
b.value ^= c.value
b.value <<<= 7
}
Обратите внимание на наличие специального оператора <<<=, который выполняет круговое смещение битов. То есть старшие биты становятся младшими вместо обнуления младших. Так исключается потеря данных во время операций.
import Foundation
// MARK: - BinaryIntegerExtensions
infix operator <<< : BitwiseShiftPrecedence
extension BinaryInteger where Self: UnsignedInteger {
static func <<<(target: Self, shiftAmount: Int) -> Self {
guard shiftAmount >= 0 else {
return target >>> -shiftAmount
}
return (target << shiftAmount) | (target >> (target.bitWidth - shiftAmount))
}
}
infix operator <<<= : BitwiseShiftPrecedence
extension BinaryInteger where Self: UnsignedInteger {
static func <<<=(target: inout Self, shiftAmount: Int) {
target = target <<< shiftAmount
}
}
infix operator >>> : BitwiseShiftPrecedence
extension BinaryInteger where Self: UnsignedInteger {
static func >>>(target: Self, shiftAmount: Int) -> Self {
guard shiftAmount >= 0 else {
return target <<< -shiftAmount
}
return (target >> shiftAmount) | (target << (target.bitWidth - shiftAmount))
}
}
infix operator >>>= : BitwiseShiftPrecedence
extension BinaryInteger where Self: UnsignedInteger {
static func >>>=(target: inout Self, shiftAmount: Int) {
target = target >>> shiftAmount
}
}
RFC-7539 секция 2.3. Метод по формированию и наложению битовой маски blockFunction(counter:). Здесь используется state, определенный пользователем ключом и nonce во время инициализации.
private func blockFunction(counter: Int) -> [Word] {
let counterIndex = 12
state[counterIndex] = Word(UInt32(counter))
var workingState: [Word] = []
for word in state {
let value = word.value
let workingWord = Word(value)
workingState.append(workingWord)
}
for _ in 0..<10 {
quarterRound(workingState[0], workingState[4], workingState[8], workingState[12])
quarterRound(workingState[1], workingState[5], workingState[9], workingState[13])
quarterRound(workingState[2], workingState[6], workingState[10], workingState[14])
quarterRound(workingState[3], workingState[7], workingState[11], workingState[15])
quarterRound(workingState[0], workingState[5], workingState[10], workingState[15])
quarterRound(workingState[1], workingState[6], workingState[11], workingState[12])
quarterRound(workingState[2], workingState[7], workingState[8], workingState[13])
quarterRound(workingState[3], workingState[4], workingState[9], workingState[14])
}
for index in 0..<state.count {
let state = state[index]
let workingState = workingState[index]
workingState.value &+= state.value
}
return workingState
}
Как говорилось выше, количество итераций выполнения цикла определяется разработчиком и может иметь любые значения. Кстати, алгоритм называется ChaCha20 как раз-таки потому, что здесь происходит круговое смещение битов 10 раз для каждой из колон workingState и 10 раз для диагоналей workingState.
RFC-7539 секция 2.4. Криптование данных реализовано блоками по 64 байта. Входные данные разбиваются на блоки, для каждого из которых формируется своя битовая маска, которая после накладывается на блок.
func encrypt(_ message: [Byte]) -> [Byte] {
var result: [Byte] = []
let blockSize = 64
var mask: [Byte] = []
var j = 0
var counter = 0
for i in 0..<message.count {
if i % blockSize == 0 {
j = 0
counter += 1
let workingState = blockFunction(counter: counter)
mask = workingState.reduce(into: []) { partialResult, word in
let maskBytes = word.getBytes()
partialResult.append(contentsOf: maskBytes)
}
}
let encryptedByte = message[i] ^ mask[j]
result.append(encryptedByte)
j += 1
}
return result
}
И здесь тоже, обратите внимание на то, что значение счетчика операций определяется разработчиком и может иметь любые значения. В данной статье все константы были указаны в соответсвии с требованиями документа RFC-7539. Манипуляции по модификации константных значений носят лишь рекомендательный характер.
Заключение
Ядром алгоритма шифрования является в применение операции исключающее или. И это достаточно разумно, поскольку отсутствует необходимость в разработке метода дешифрования. Также, преимуществом является возможность определения собственных значений некоторых констант, что позволяет выполнить более тонкую настройку для нужд разработчика.
И напоследок, стоит напомнить о том, что nonce рекомендуется регулярно обновлять, а не использовать одни и те же данные. Но это уже другая, более широкая тема, которая выходит за рамки этой статьи.
Ссылки
Оригинал статьи