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>&nbsp;</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

Questions or comments?

Contact me!