Part 1 and Part 2 of this series look at the Vigenère Cipher, how it's created, and how it's used. Part 3 presents an implementation in PHP.
Unlike JavaScript, PHP is not multi-byte safe by default. A naive implementation will successfully encipher and decipher messages made up of ASCII characters where the alphabet and keywords are also made up of ASCII characters. This implementation specifically uses multi-byte safe functions for string handling, so that limitation is avoided.
Note that the function mb_str_split() is used here, but is only supported in PHP 7.4 and above. A polyfill is provided in the comments if you want to try this in PHP 7.3 or lower.
By default, PHP is configured to use UTF-8 for it's default character set. I see no reason why this code shouldn't work with other multi-byte formats, but this has not been tested.
The code!
See below for usage
<?php
/**
* Cipher_class.php
*
* An implementation of the Vigenère cipher
*
* @author Mike Whipps
* @copyright Copyright (c) 2021 Mike Whipps, littlevale.com
*/
class Cipher {
/**
* @var array store the array of ciphers
*/
private array $cipher = [];
/**
* @var array store the array of alphabet letters
*/
private array $alphabet;
/**
* @var array store the array of passcode letters
*/
private array $passCode;
/**
* Splits a multibyte string into an array of characters. Comparable to str_split().
* From PHP 7.4 onwards this function is native to PHP, so this is provided only
* for reference.
*
* @param $string
*
* @return array|false|string[]
*/
/**
private function mb_str_split( $string ):array {
# Split at all position not after the start: ^
# and not before the end: $
return preg_split('/(?<!^)(?!$)/u', $string );
}
*/
/**
* Create a code table based on the provided alphabet
*
* @param $alphabet
*/
private function createCodeTable(array $alphabet):void {
// Iterate over the provided alphabet, to create a row/column grid indexed
// by character. Rows are the first index, columns the second
$gridRow = $this->alphabet;
foreach($this->alphabet as $char) {
$this->cipher[$char] = array_combine($this->alphabet, $gridRow);
// Rotate the grid row for the next line
array_push($gridRow, array_shift($gridRow));
}
}
public function __construct(string $alphabet, string $passCode) {
if (empty($alphabet)) {
throw new InvalidArgumentException("No pass code set");
}
// Split the alphabet string into an array.
// This makes it easier to index into it and check
// codeword and cipher strings against it.
$this->alphabet = mb_str_split($alphabet);
// create the enciphering table
$this->createCodeTable($this->alphabet);
// Set the passcode for encryption or decryption.
// Check that all the passcode characters are contained in the alphabet.
// If not, throw an exception.
if (empty($passCode)) {
throw new InvalidArgumentException("No pass code set");
}
$this->passCode = mb_str_split($passCode);
if (array_diff($this->passCode, $this->alphabet)) {
throw new InvalidArgumentException("Bad Passcode: characters not in alphabet");
}
}
/**
* Create a representation of the cipher grid in text, each row delimited by a newline.
* @return string
*/
public function getTextGrid():string {
$grid = " | ".implode(' ', $this->alphabet).PHP_EOL;
$grid .= "--|".str_repeat('---', sizeof($this->alphabet)).PHP_EOL;
foreach($this->cipher as $key=> $row) {
$grid .= $key." | ".implode(' ',$row).PHP_EOL;
}
return $grid;
}
/**
* @return string
*/
public function getHTMLGrid():string {
$grid = '<table><thead><tr><th> </th><td>'.implode('</td><td>',$this->alphabet).'</td></tr></thead>'.PHP_EOL;
$grid .= '<tbody>';
foreach($this->cipher as $key=>$row) {
$grid .= "<tr><th>$key</th><td>".implode('</td><td>',$row).'</td></tr>'.PHP_EOL;
}
$grid .= '</tbody></table>';
return $grid;
}
/**
* Encipher the message.
* Any character not found in the alphabet will be added to the enciphered string as is.
*
* @param string $str
*
* @return string
*/
public function encipher(string $str):string {
if (empty($str)) {
throw new InvalidArgumentException();
}
// Split string into an array. Required because array syntax on strings is not multi-byte safe.
$strArray = mb_str_split($str);
$cipher = '';
for($i = 0; $i < mb_strlen($str); $i++) {
$nextCode = $i % count($this->passCode) ;
$cipher .= $this->cipher[$this->passCode[$nextCode]][$strArray[$i]] ?? $strArray[$i];
}
return $cipher;
}
/**
* Decipher the coded message.
* Any character not found in the alphabet will be added to the deciphered string as is.
*
* @param string $str
*
* @return string
*/
public function decipher(string $str):string {
if (empty($str)) {
throw new InvalidArgumentException();
}
// Split string into an array. Required because array syntax on strings is not multi-byte safe.
$strArray = mb_str_split($str);
$cipher = '';
for($i = 0; $i < mb_strlen($str, 'UTF-8'); $i++) {
$nextCode = $i % count($this->passCode);
// array_search returns false if it doesn't find the value, but it might also return a false-y value (like zero)
// so we test explicitly for false
$decode = array_search($strArray[$i], $this->cipher[$this->passCode[$nextCode]]);
$cipher .= ($decode !== false) ? $decode : $strArray[$i];
}
return $cipher;
}
}
Usage
<?php
include("Cipher_class.php");
// Create a cipher
$cipher = new Cipher("ABCDEFGHIJKLMNOPQRSTUVWXYZ", "KEYWORD");
$textGrid = $cipher->getTextGrid(); // return a simple text grid suitable for use in <pre> block
$htmlGrid = $cipher->getHTMLGrid(); // return a grid in an HTML table that can be formatted with CSS
$code = $cipher->encipher("SECRETMESSAGEHERE"); // return an enciphered version of the message
$decode = $ciipher->decipher("CODEDMESSAGE"); // decipher the message and return the plain text version.
Get the Code!
You can download the code here