using System;
using System.Collections.Generic;
using System.Text;
namespace Algorithms.Encoders
{
/// <summary>
/// Encodes using Feistel cipher.
/// https://en.wikipedia.org/wiki/Feistel_cipher
/// In cryptography, a Feistel cipher (also known as Luby–Rackoff block cipher)
/// is a symmetric structure used in the construction of block ciphers,
/// named after the German-born physicist and cryptographer Horst Feistel
/// who did pioneering research while working for IBM (USA)
/// A large proportion of block ciphers use the scheme, including the US DES,
/// the Soviet/Russian GOST and the more recent Blowfish and Twofish ciphers.
/// </summary>
public class FeistelCipher : IEncoder<uint>
{
// number of rounds to transform data block, each round a new "round" key is generated.
private const int Rounds = 32;
/// <summary>
/// Encodes text using specified key,
/// where n - text length.
/// </summary>
/// <param name="text">Text to be encoded.</param>
/// <param name="key">Key that will be used to encode the text.</param>
/// <exception cref="ArgumentException">Error: key should be more than 0x00001111 for better encoding, key=0 will throw DivideByZero exception.</exception>
/// <returns>Encoded text.</returns>
public string Encode(string text, uint key)
{
List<ulong> blocksListPlain = SplitTextToBlocks(text);
StringBuilder encodedText = new();
foreach (ulong block in blocksListPlain)
{
uint temp = 0;
// decompose a block to two subblocks 0x0123456789ABCDEF => 0x01234567 & 0x89ABCDEF
uint rightSubblock = (uint)(block & 0x00000000FFFFFFFF);
uint leftSubblock = (uint)(block >> 32);
uint roundKey;
// Feistel "network" itself
for (int round = 0; round < Rounds; round++)
{
roundKey = GetRoundKey(key, round);
temp = rightSubblock ^ BlockModification(leftSubblock, roundKey);
rightSubblock = leftSubblock;
leftSubblock = temp;
}
// compile text string formating the block value to text (hex based), length of the output = 16 byte always
ulong encodedBlock = leftSubblock;
encodedBlock = (encodedBlock << 32) | rightSubblock;
encodedText.Append(string.Format("{0:X16}", encodedBlock));
}
return encodedText.ToString();
}
/// <summary>
/// Decodes text that was encoded using specified key.
/// </summary>
/// <param name="text">Text to be decoded.</param>
/// <param name="key">Key that was used to encode the text.</param>
/// <exception cref="ArgumentException">Error: key should be more than 0x00001111 for better encoding, key=0 will throw DivideByZero exception.</exception>
/// <exception cref="ArgumentException">Error: The length of text should be divisible by 16 as it the block lenght is 16 bytes.</exception>
/// <returns>Decoded text.</returns>
public string Decode(string text, uint key)
{
// The plain text will be padded to fill the size of block (16 bytes)
if (text.Length % 16 != 0)
{
throw new ArgumentException($"The length of {nameof(key)} should be divisible by 16");
}
List<ulong> blocksListEncoded = GetBlocksFromEncodedText(text);
StringBuilder decodedTextHex = new();
foreach (ulong block in blocksListEncoded)
{
uint temp = 0;
// decompose a block to two subblocks 0x0123456789ABCDEF => 0x01234567 & 0x89ABCDEF
uint rightSubblock = (uint)(block & 0x00000000FFFFFFFF);
uint leftSubblock = (uint)(block >> 32);
// Feistel "network" - decoding, the order of rounds and operations on the blocks is reverted
uint roundKey;
for (int round = Rounds - 1; round >= 0; round--)
{
roundKey = GetRoundKey(key, round);
temp = leftSubblock ^ BlockModification(rightSubblock, roundKey);
leftSubblock = rightSubblock;
rightSubblock = temp;
}
// compose decoded block
ulong decodedBlock = leftSubblock;
decodedBlock = (decodedBlock << 32) | rightSubblock;
for(int i = 0; i < 8; i++)
{
ulong a = (decodedBlock & 0xFF00000000000000) >> 56;
// it's a trick, the code works with non zero characters, if your text has ASCII code 0x00 it will be skipped.
if (a != 0)
{
decodedTextHex.Append((char)a);
}
decodedBlock = decodedBlock << 8;
}
}
return decodedTextHex.ToString();
}
// Using the size of block = 8 bytes this function splts the text and returns set of 8 bytes (ulong) blocks
// the last block is extended up to 8 bytes if the tail of the text is smaller than 8 bytes
private static List<ulong> SplitTextToBlocks(string text)
{
List<ulong> blocksListPlain = new();
byte[] textArray = Encoding.ASCII.GetBytes(text);
int offset = 8;
for(int i = 0; i < text.Length; i += 8)
{
// text not always has len%16 == 0, that's why the offset should be adjusted for the last part of the text
if (i > text.Length - 8)
{
offset = text.Length - i;
}
string block = Convert.ToHexString(textArray, i, offset);
blocksListPlain.Add(Convert.ToUInt64(block, 16));
}
return blocksListPlain;
}
// convert the encoded text to the set of ulong values (blocks for decoding)
private static List<ulong> GetBlocksFromEncodedText(string text)
{
List<ulong> blocksListPlain = new();
for(int i = 0; i < text.Length; i += 16)
{
ulong block = Convert.ToUInt64(text.Substring(i, 16), 16);
blocksListPlain.Add(block);
}
return blocksListPlain;
}
// here might be any deterministic math formula
private static uint BlockModification(uint block, uint key)
{
for (int i = 0; i < 32; i++)
{
// 0x55555555 for the better distribution 0 an 1 in the block
block = ((block ^ 0x55555555) * block) % key;
block = block ^ key;
}
return block;
}
// There are many ways to generate a round key, any deterministic math formula does work
private static uint GetRoundKey(uint key, int round)
{
// "round + 2" - to avoid a situation when pow(key,1) ^ key = key ^ key = 0
uint a = (uint)Math.Pow((double)key, round + 2);
return a ^ key;
}
}
}