Bifid cipher

From Rosetta Code
Revision as of 15:30, 30 June 2023 by PSNOW123 (talk | contribs) (Corrected spelling mistake.)
Task
Bifid cipher
You are encouraged to solve this task according to the task description, using any language you may know.
Description

The Bifid cipher is a polygraphic substitution cipher which was invented by Félix Delastelle in around 1901. It uses a 5 x 5 Polybius square combined with transposition and fractionation to encrypt a message. Any 5 x 5 Polybius square can be used but, as it only has 25 cells and there are 26 letters of the (English) alphabet, one cell needs to represent two letters - I and J being a common choice.

Operation

Suppose we want to encrypt the message "ATTACKATDAWN".

We use this archetypal Polybius square where I and J share the same position.

x/y 1 2 3 4 5
-------------
1 | A B C D E
2 | F G H I K
3 | L M N O P
4 | Q R S T U 
5 | V W X Y Z

The message is first converted to its x, y coordinates, but they are written vertically beneath.

A T T A C K A T D A W N
1 4 4 1 1 2 1 4 1 1 5 3
1 4 4 1 3 5 1 4 4 1 2 3

They are then arranged in a row.

1 4 4 1 1 2 1 4 1 1 5 3 1 4 4 1 3 5 1 4 4 1 2 3

Finally, they are divided up into pairs which are used to look up the encrypted letters in the square.

14 41 12 14 11 53 14 41 35 14 41 23
D  Q  B  D  A  X  D  Q  P  D  Q  H

The encrypted message is therefore "DQBDAXDQPDQH".

Decryption can be achieved by simply reversing these steps.

Task

Write routines in your language to encrypt and descrypt a message using the Bifid cipher.

Use them to verify (including subsequent decryption):

1. The above example.

2. The example in the Wikipedia article using the message and Polybius square therein.

3. The above example but using the Polybius square in the Wikipedia article to illustrate that it doesn't matter which square you use as long, of course, as the same one is used for both encryption and decryption.

In addition, encrypt and decrypt the message "The invasion will start on the first of January" using any Polybius square you like. Convert the message to upper case and ignore spaces.

Bonus

Suggest a way in which the cipher could be modified so that ALL 26 letters can be uniquely encrypted.

Related task

Playfair cipher

Ada

-- Bifid cipher
-- J. Carter     2023 May
-- The Bifid cipher is included as part of the PragmAda Reusable Components (https://github.com/jrcarter/PragmARC)

with Ada.Text_IO;
with PragmARC.Encryption.Bifid;

procedure Bifid_Test is
   package Bifid renames PragmARC.Encryption.Bifid;

   Key_1 : constant Bifid.Square_Layout := ("ABCDE", "FGHIK", "LMNOP", "QRSTU", "VWXYZ");
   Key_2 : constant Bifid.Square_Layout := ("BGWKZ", "QPNDS", "IOAXE", "FCLUM", "THYVR");

   Msg_1 : constant String := "ATTACKATDAWN";
   Msg_2 : constant String := "FLEEATONCE";
   Msg_3 : constant String := "THEINVASIONWILLSTARTONTHEFIRSTOFJANUARY";

   Crypt_1 : String (Msg_1'Range);
   Crypt_2 : String (Msg_2'Range);
   Crypt_3 : String (Msg_3'Range);
begin -- Bifid_Test
   Crypt_1 := Bifid.Encrypt (Msg_1, Key_1);
   Ada.Text_IO.Put_Line (Item => Msg_1 & " => " & Crypt_1 & " => " & Bifid.Decrypt (Crypt_1, Key_1) );
   Crypt_2 := Bifid.Encrypt (Msg_2, Key_2);
   Ada.Text_IO.Put_Line (Item => Msg_2 & " => " & Crypt_2 & " => " & Bifid.Decrypt (Crypt_2, Key_2) );
   Crypt_1 := Bifid.Encrypt (Msg_1, Key_2);
   Ada.Text_IO.Put_Line (Item => Msg_1 & " => " & Crypt_1 & " => " & Bifid.Decrypt (Crypt_1, Key_2) );
   Crypt_3 := Bifid.Encrypt (Msg_3, Key_2);
   Ada.Text_IO.Put_Line (Item => Msg_3 & " => " & Crypt_3 & " => " & Bifid.Decrypt (Crypt_3, Key_2) );
end Bifid_Test;
Output:
ATTACKATDAWN => DQBDAXDQPDQH => ATTACKATDAWN
FLEEATONCE => UAEOLWRINS => FLEEATONCE
ATTACKATDAWN => EYFENGIWDILA => ATTACKATDAWN
THEINVASIONWILLSTARTONTHEFIRSTOFJANUARY => RASOAQXCYRORXESXADETSWLTNIATEGISBRGBALY => THEINVASIONWILLSTARTONTHEFIRSTOFIANUARY

AppleScript

(* The "square" here is only notional, the characters' coordinates being calculated
   from their offsets in a linear text key. But a square key is accepted if the
   signaller's careless enough to keep or transmit one in this form. *)
on bifidEncipher(message, square)
    return transcipher(message, square, 1)
end bifidEncipher

on bifidDecipher(message, square)
    return transcipher(message, square, 2)
end bifidDecipher

on transcipher(message, square, code) -- code: 1 = encipher, 2 = decipher.
    -- Check and massage the input.
    set message to join(message's words, "")
    if (message contains ".") then set message to replaceText(".", "", message)
    set square to join(join(square, "")'s words, "")
    set squarea to (count square)
    set sqrt to (squarea ^ 0.5) as integer
    if (sqrt * sqrt  squarea) then error "Invalid key."
    ignoring case
        if ((sqrt < 6) and (message contains "J")) then ¬
            set message to replaceText("J", "I", message)
    end ignoring
    -- Calculate coordinates from the message characters' offsets in the "square" text
    -- and either split and recombine them (when enciphering) or not (deciphering).
    set list1 to {}
    set list2 to {{}, list1}'s item code
    set astid to AppleScript's text item delimiters
    ignoring case and diacriticals
        repeat with chr in message
            set AppleScript's text item delimiters to chr
            set |offset - 1| to (count square's first text item)
            set list1's end to |offset - 1| div sqrt + 1
            set list2's end to |offset - 1| mod sqrt + 1
        end repeat
    end ignoring
    set AppleScript's text item delimiters to astid
    set coords to {list1 & list2, list1}'s item code
    -- Calculate new offsets from the appropriate numbers in the coords list
    -- and get the characters offset by those amounts in the "square" text.
    set chrs to {}
    set nCoords to (count coords)
    set indexDifference to {1, nCoords div 2}'s item code
    repeat with i from 1 to (nCoords div code) by (3 - code)
        set |offset| to ((coords's item i) - 1) * sqrt + (coords's item (i + indexDifference))
        set chrs's end to square's character |offset|
    end repeat
    return join(chrs, "")
end transcipher

on join(lst, delim)
    set astid to AppleScript's text item delimiters
    set AppleScript's text item delimiters to delim
    set txt to lst as text
    set AppleScript's text item delimiters to astid
    return txt
end join

on replaceText(searchText, replacement, mainText)
    set astid to AppleScript's text item delimiters
    set AppleScript's text item delimiters to searchText
    set textItems to mainText's text items
    set AppleScript's text item delimiters to replacement
    set mainText to textItems as text
    set AppleScript's text item delimiters to astid
    return mainText
end replaceText

on task()
    set output to {}
    repeat with this in {{"Given example:", "ATTACKATDAWN", "ABCDEFGHIKLMNOPQRSTUVWXYZ"}, ¬
        {"Wikipedia example:", "FLEEATONCE", {"BGWKZ", "QPNDS", "IOAXE", "FCLUM", "THYVR"}}, ¬
        {"A 6 x 6 square allows all 36 letters & digits to be encoded:", ¬
            "The invasion will start on the 1st of January", ¬
            "THEQUI
                    9CKBR0
                    1OWNF8
                    7XJMP2
                    3SVLA6
                    5ZYDG4"}}
        set {heading, message, square} to this
        set encrypted to bifidEncipher(message, square)
        set decrypted to bifidDecipher(encrypted, square)
        set output's end to heading
        set output's end to message & " --> " & encrypted & " --> " & decrypted
    end repeat
    
    return join(output, linefeed)
end task

task()
Output:
"Given example:
ATTACKATDAWN --> DQBDAXDQPDQH --> ATTACKATDAWN
Wikipedia example:
FLEEATONCE --> UAEOLWRINS --> FLEEATONCE
A 6 x 6 square allows all 36 letters & digits to be encoded:
The invasion will start on the 1st of January --> TTFAEWUAU9WTE3WP1S5KDF0B8M9AH7KHHVLAV --> THEINVASIONWILLSTARTONTHE1STOFJANUARY"

C++

#include <iostream>
#include <stdexcept>
#include <string>
#include <unordered_map>
#include <vector>

typedef std::pair<int32_t, int32_t> point;

class Bifid {
public:
	Bifid(const int32_t& n, const std::string& text) {
		if ( text.length() != n * n ) {
			throw new std::invalid_argument("Incorrect length of text");
		}

		grid.resize(n);
		for ( uint64_t i = 0; i < grid.size(); i++ ) {
			grid[i].resize(n);
		}

		int32_t row = 0;
		int32_t col = 0;
		for ( char ch : text ) {
			grid[row][col] = ch;
			coordinates[ch] = point(row, col);
			col += 1;
			if ( col == n ) {
				col = 0;
				row += 1;
			 }
		 }

		 if ( n == 5 ) {
			 coordinates['J'] = coordinates['I'];
		 }
	}

	std::string encrypt(const std::string& text) {
		std::vector<int32_t> row_one, row_two;
		for ( char ch : text ) {
			point coordinate = coordinates[ch];
			row_one.push_back(coordinate.first);
			row_two.push_back(coordinate.second);
		 }

		 row_one.insert(row_one.end(), row_two.begin(), row_two.end());
		 std::string result;
		 for ( uint64_t i = 0; i < row_one.size() - 1; i += 2 ) {
			 result += grid[row_one[i]][row_one[i + 1]];
		 }
		 return result;
	}

	std::string decrypt(const std::string& text) {
		std::vector<int32_t> row;
		for ( char ch : text ) {
			point coordinate = coordinates[ch];
			row.push_back(coordinate.first);
			row.push_back(coordinate.second);
		}

		const int middle = row.size() / 2;
		std::vector<int32_t> row_one = { row.begin(), row.begin() + middle };
		std::vector<int32_t> row_two = { row.begin() + middle, row.end() };
		std::string result;
		for ( int32_t i = 0; i < middle; i++ ) {
			result += grid[row_one[i]][row_two[i]];
		}
		return result;
	}

	void display() const {
		for ( std::vector<char> row : grid ) {
			for ( char ch : row ) {
				std::cout << ch << " ";
			}
			std::cout << std::endl;
		}
	}
private:
	std::vector<std::vector<char>> grid;
	std::unordered_map<char, point> coordinates;
};

void runTest(Bifid bifid, std::string message) {
	std::cout << "Using Polybius square:" << std::endl;
	bifid.display();
	std::cout << "Message:   " << message << std::endl;
	std::string encrypted = bifid.encrypt(message);
	std::cout << "Encrypted: " << encrypted << std::endl;
	std::string decrypted = bifid.decrypt(encrypted);
	std::cout << "Decrypted: " << decrypted << std::endl;
	std::cout << std::endl;
}

int main() {
	const std::string message1 = "ATTACKATDAWN";
	const std::string message2 = "FLEEATONCE";
	const std::string message3 = "THEINVASIONWILLSTARTONTHEFIRSTOFJANUARY";

	Bifid bifid1(5, "ABCDEFGHIKLMNOPQRSTUVWXYZ");
	Bifid bifid2(5, "BGWKZQPNDSIOAXEFCLUMTHYVR");

	runTest(bifid1, message1);
	runTest(bifid2, message2);
	runTest(bifid2, message1);
	runTest(bifid1, message2);

	Bifid bifid3(6, "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789");
	runTest(bifid3, message3);
}
Output:
Using Polybius square:
A B C D E 
F G H I K 
L M N O P 
Q R S T U 
V W X Y Z 
Message:   ATTACKATDAWN
Encrypted: DQBDAXDQPDQH
Decrypted: ATTACKATDAWN

Using Polybius square:
B G W K Z 
Q P N D S 
I O A X E 
F C L U M 
T H Y V R 
Message:   FLEEATONCE
Encrypted: UAEOLWRINS
Decrypted: FLEEATONCE

Using Polybius square:
B G W K Z 
Q P N D S 
I O A X E 
F C L U M 
T H Y V R 
Message:   ATTACKATDAWN
Encrypted: EYFENGIWDILA
Decrypted: ATTACKATDAWN

Using Polybius square:
A B C D E 
F G H I K 
L M N O P 
Q R S T U 
V W X Y Z 
Message:   FLEEATONCE
Encrypted: HADNAAZDSP
Decrypted: FLEEATONCE

Using Polybius square:
A B C D E F 
G H I J K L 
M N O P Q R 
S T U V W X 
Y Z 0 1 2 3 
4 5 6 7 8 9 
Message:   THEINVASIONWILLSTARTONTHEFIRSTOFJANUARY
Encrypted: TBPDIPHJSPOTAIVMGPCZKNSCN09BFIHK64I7BM4
Decrypted: THEINVASIONWILLSTARTONTHEFIRSTOFJANUARY

J

Implementation:

alpha=: a.{~65+i.26
normalize=: {{ rplc&'JI'(toupper y)([-.-.)alpha }}
bifid=: {{ m{~_2 (t&#.)\,|:(t,t=.%:#m)#:m i.y([-.-.)m }}
difib=: {{ m{~t#.|:(|.@$$,)(t,t=.%:#m)#:m i.y([-.-.)m }}

This is pretty much a literal implementation of the algorithm, except that our indices range from 0..4 instead of 1..5 (or, for letter indices, they range from 0..24 instead of 1..25).

Much of the implementation is about converting between a two digit base 5 representation and a single digit numeric representation. The rest is simple array manipulations to make the rest come out right.

Task examples:

ref1=: ~.normalize alpha
ref2=: 'BGWKZQPNDSIOAXEFCLUMTHYVR'
ref3=: 'PLAYFIREXMBCDGHKNOQSTUVWZ'

   ref1 bifid normalize 'attack at dawn'
DQBDAXDQPDQH
   ref1 difib ref1 bifid normalize 'attack at dawn'
ATTACKATDAWN
   ref2 bifid normalize 'flee at once'
UAEOLWRINS
   ref2 difib ref2 bifid normalize 'flee at once'
FLEEATONCE
   ref2 bifid normalize 'attack at dawn'
EYFENGIWDILA
   ref2 difib ref2 bifid normalize 'attack at dawn'
ATTACKATDAWN
   ref3 bifid normalize 'The invasion will start on the first of January'
VRSYXSIYTMQVIRSKISLPVLDTCKRTCAIVTMATCEX
   ref3 difib ref3 bifid normalize 'The invasion will start on the first of January'
THEINVASIONWILLSTARTONTHEFIRSTOFIANUARY
   (_81{.123{.a.)bifid 'The invasion will start on the first of January'
TgqhpqpqxzpxfqzoKqhxw/O3eWH53BYw`+Be8F1
   (_81{.123{.a.)difib(_81{.123{.a.)bifid 'The invasion will start on the first of January'
TheinvasionwillstartonthefirstofJanuary

Java

import java.awt.Point;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public final class BifidCipher {

	public static void main(String[] aArgs) {
		final String message1 = "ATTACKATDAWN";
		final String message2 = "FLEEATONCE";
		final String message3 = "The invasion will start on the first of January".toUpperCase().replace(" ", "");
		
		Bifid bifid1 = new Bifid(5, "ABCDEFGHIKLMNOPQRSTUVWXYZ");
		Bifid bifid2 = new Bifid(5, "BGWKZQPNDSIOAXEFCLUMTHYVR");
		
		runTest(bifid1, message1);
		runTest(bifid2, message2);
		runTest(bifid2, message1);
		runTest(bifid1, message2);
		
		Bifid bifid3 = new Bifid(6, "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789");
		runTest(bifid3, message3);
	}	
	
	private static void runTest(Bifid aBifid, String aMessage) {
		System.out.println("Using Polybius square:");
		aBifid.display();
		System.out.println("Message:   " + aMessage);
		String encrypted = aBifid.encrypt(aMessage);
		System.out.println("Encrypted: " + encrypted);
		String decrypted = aBifid.decrypt(encrypted);
		System.out.println("Decrypted: " + decrypted);
		System.out.println();
	}	
	
}
	
final class Bifid {
	
	public Bifid(int aN, String aText) {
		if ( aText.length() != aN * aN ) {
			throw new IllegalArgumentException("Incorrect length of text");
		}
		
		grid = new char[aN][aN];			
		int row = 0;
		int col = 0;		 
		for ( char ch : aText.toCharArray() ) {
		    grid[row][col] = ch;
		    coordinates.put(ch, new Point(row, col) );
		    col += 1;
		    if ( col == aN ) {
		    	col = 0;
		    	row += 1;
		     }
		 }
		
		 if ( aN == 5 ) {
		     coordinates.put('J', coordinates.get('I'));
		 }
	}	
	
	public String encrypt(String aText) {		 
		List<Integer> rowOne = new ArrayList<Integer>();
		List<Integer> rowTwo = new ArrayList<Integer>();
		for ( char ch : aText.toCharArray() ) {
			Point coordinate = coordinates.get(ch);
			rowOne.add(coordinate.x);
			rowTwo.add(coordinate.y);
		 }
		  
		 rowOne.addAll(rowTwo);
		 StringBuilder result = new StringBuilder();
		 for ( int i = 0; i < rowOne.size() - 1; i += 2 ) {
			 result.append(grid[rowOne.get(i)][rowOne.get(i + 1)]);
		 }
		 return result.toString();
	}
	
	public String decrypt(String aText) {
		List<Integer> row = new ArrayList<Integer>();
		for ( char ch : aText.toCharArray() ) {
			Point coordinate = coordinates.get(ch);
		    row.add(coordinate.x);
		    row.add(coordinate.y);
		}
		
		final int middle = row.size() / 2;
		List<Integer> rowOne = row.subList(0, middle);
		List<Integer> rowTwo = row.subList(middle, row.size());
		StringBuilder result = new StringBuilder();
		for ( int i = 0; i < middle; i++ ) {
			result.append(grid[rowOne.get(i)][rowTwo.get(i)]);
		}
		return result.toString();
	}

    public void display() {
		Arrays.stream(grid).forEach( row -> System.out.println(Arrays.toString(row)) );
	}
	
	private char[][] grid;
	private Map<Character, Point> coordinates = new HashMap<Character, Point>();
	
}
Output:
Using Polybius square:
[A, B, C, D, E]
[F, G, H, I, K]
[L, M, N, O, P]
[Q, R, S, T, U]
[V, W, X, Y, Z]
Message:   ATTACKATDAWN
Encrypted: DQBDAXDQPDQH
Decrypted: ATTACKATDAWN

Using Polybius square:
[B, G, W, K, Z]
[Q, P, N, D, S]
[I, O, A, X, E]
[F, C, L, U, M]
[T, H, Y, V, R]
Message:   FLEEATONCE
Encrypted: UAEOLWRINS
Decrypted: FLEEATONCE

Using Polybius square:
[B, G, W, K, Z]
[Q, P, N, D, S]
[I, O, A, X, E]
[F, C, L, U, M]
[T, H, Y, V, R]
Message:   ATTACKATDAWN
Encrypted: EYFENGIWDILA
Decrypted: ATTACKATDAWN

Using Polybius square:
[A, B, C, D, E]
[F, G, H, I, K]
[L, M, N, O, P]
[Q, R, S, T, U]
[V, W, X, Y, Z]
Message:   FLEEATONCE
Encrypted: HADNAAZDSP
Decrypted: FLEEATONCE

Using Polybius square:
[A, B, C, D, E, F]
[G, H, I, J, K, L]
[M, N, O, P, Q, R]
[S, T, U, V, W, X]
[Y, Z, 0, 1, 2, 3]
[4, 5, 6, 7, 8, 9]
Message:   THEINVASIONWILLSTARTONTHEFIRSTOFJANUARY
Encrypted: TBPDIPHJSPOTAIVMGPCZKNSCN09BFIHK64I7BM4
Decrypted: THEINVASIONWILLSTARTONTHEFIRSTOFJANUARY

Julia

Using the Raku example's test messages.

polybius(text) = Char.(reshape(Int.(collect(text)), isqrt(length(text)), :)')

function encrypt(message, poly)
    positions = [findall(==(c), poly)[1] for c in message]
    numbers = vcat([c[1] for c in positions], [c[2] for c in positions])
    return String([poly[numbers[i], numbers[i+1]] for i in 1:2:length(numbers)-1])
end

function decrypt(message, poly)
   n = length(message)
   positions = [findall(==(c), poly)[1] for c in message]
   numbers = reduce(vcat, [[c[1], c[2]] for c in positions])
   return String([poly[numbers[i], numbers[i+n]] for i in 1:n])
end


for (key, text) in [("ABCDEFGHIKLMNOPQRSTUVWXYZ", "ATTACKATDAWN"), ("BGWKZQPNDSIOAXEFCLUMTHYVR", "FLEEATONCE"),
   ([' '; '.'; 'A':'Z'; 'a':'z'; '0':'9'], "The invasion will start on the first of January 2023.")]
    poly = polybius(key)
    encrypted = encrypt(text, poly)
    decrypted = decrypt(encrypted, poly)
    println("Using polybius:")
    display(poly)
    println("\n  Message: $text\n    Encrypted: $encrypted\n    Decrypted: $decrypted\n\n")
end
Output:
Using polybius:
5×5 Matrix{Char}:
 'A'  'B'  'C'  'D'  'E'
 'F'  'G'  'H'  'I'  'K'
 'L'  'M'  'N'  'O'  'P'
 'Q'  'R'  'S'  'T'  'U'
 'V'  'W'  'X'  'Y'  'Z'

  Message: ATTACKATDAWN
    Encrypted: DQBDAXDQPDQH
    Decrypted: ATTACKATDAWN


Using polybius:
5×5 Matrix{Char}:
 'B'  'G'  'W'  'K'  'Z'
 'Q'  'P'  'N'  'D'  'S'
 'I'  'O'  'A'  'X'  'E'
 'F'  'C'  'L'  'U'  'M'
 'T'  'H'  'Y'  'V'  'R'

  Message: FLEEATONCE
    Encrypted: UAEOLWRINS
    Decrypted: FLEEATONCE


Using polybius:
8×8 Matrix{Char}:
 ' '  '.'  'A'  'B'  'C'  'D'  'E'  'F'
 'G'  'H'  'I'  'J'  'K'  'L'  'M'  'N'
 'O'  'P'  'Q'  'R'  'S'  'T'  'U'  'V'
 'W'  'X'  'Y'  'Z'  'a'  'b'  'c'  'd'
 'e'  'f'  'g'  'h'  'i'  'j'  'k'  'l'
 'm'  'n'  'o'  'p'  'q'  'r'  's'  't'
 'u'  'v'  'w'  'x'  'y'  'z'  '0'  '1'
 '2'  '3'  '4'  '5'  '6'  '7'  '8'  '9'

  Message: The invasion will start on the first of January 2023.
    Encrypted: SejxqrEierbmrDiCjrDeJsbu89DWCHkgGS9E6tAG5 Ks2PBfCq uH
    Decrypted: The invasion will start on the first of January 2023.

Nim

To add the letter J, we use a 6x6 Polybius square containing the 26 letters and the 10 digits. To build the square, we use a generic type parameterized with the side length.

import std/[sequtils, strutils, tables]

# Description of a Bifid cipher.
type Bifid[N: static Positive] = object
  grid: array[1..N, array[1..N, char]]
  coords: Table[char, (int, int)]

proc initBifid(N: static Positive; text: string): Bifid[N] =
  # Initialize a Bifid cipher.
  assert text.len == N * N
  var row, col = 1
  for c in text:
    result.grid[row][col] = c
    result.coords[c] = (row, col)
    inc col
    if col > N:
      col = 1
      inc row
  if N == 5:
    result.coords['J'] = result.coords['I']

func encrypt(bifid: Bifid; text: string): string =
  ## Encrypt "text" using the given cipher.
  var row1, row2: seq[int]
  for ch in text:
    let coords = bifid.coords[ch]
    row1.add coords[0]
    row2.add coords[1]
  let row = row1 & row2
  for i in countup(0, row.high, 2):
    result.add bifid.grid[row[i]][row[i+1]]

func decrypt(bifid: Bifid; text: string): string =
  ## Decrypt "text" using the given cipher.
  var row: seq[int]
  for ch in text:
    let coords = bifid.coords[ch]
    row.add [coords[0],  coords[1]]
  let m = row.len shr 1
  let row1 = row[0..<m]
  let row2 = row[m..^1]
  for i in 0..<m:
    result.add bifid.grid[row1[i]][row2[i]]

func `$`(bifid: Bifid): string =
  ## Display the Polybius square of a Bifid cipher.
  result = "  " & toSeq(1..bifid.N).join(" ") & '\n'
  for row in 1..bifid.N:
    result.add alignLeft($row, 2) & bifid.grid[row].join(" ") & '\n'

proc runTest(bifid: Bifid; message: string) =
  ## Run the test with given cipher and message.
  echo "Using Polybius square:"
  echo bifid
  echo "Message:   ", message
  let encrypted = bifid.encrypt(message)
  echo "Encrypted: ", encrypted
  let decrypted = bifid.decrypt(encrypted)
  echo "Decrypted: ", decrypted
  echo("\n─────────────────────────")

const
  Message1 = "ATTACKATDAWN"
  Message2 = "FLEEATONCE"
  Message3 = "The invasion will start on the first of January".toUpperAscii.replace(" ")

# Using 5x5 Polybius squares.
const
  Bifid1 = initBifid(5, "ABCDEFGHIKLMNOPQRSTUVWXYZ")
  Bifid2 = initBifid(5, "BGWKZQPNDSIOAXEFCLUMTHYVR")
for (bifid, message) in [(Bifid1, Message1), (Bifid2, Message2),
                         (Bifid2, Message1), (Bifid1, Message3)]:
  runTest(bifid, message)

# Using a 6x6 Polybius square with 26 letters and 10 digits.
const Bifid3 = initBifid(6, "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789")
runTest(Bifid3, Message3)
Output:
Using Polybius square:
  1 2 3 4 5
1 A B C D E
2 F G H I K
3 L M N O P
4 Q R S T U
5 V W X Y Z

Message:   ATTACKATDAWN
Encrypted: DQBDAXDQPDQH
Decrypted: ATTACKATDAWN

─────────────────────────
Using Polybius square:
  1 2 3 4 5
1 B G W K Z
2 Q P N D S
3 I O A X E
4 F C L U M
5 T H Y V R

Message:   FLEEATONCE
Encrypted: UAEOLWRINS
Decrypted: FLEEATONCE

─────────────────────────
Using Polybius square:
  1 2 3 4 5
1 B G W K Z
2 Q P N D S
3 I O A X E
4 F C L U M
5 T H Y V R

Message:   ATTACKATDAWN
Encrypted: EYFENGIWDILA
Decrypted: ATTACKATDAWN

─────────────────────────
Using Polybius square:
  1 2 3 4 5
1 A B C D E
2 F G H I K
3 L M N O P
4 Q R S T U
5 V W X Y Z

Message:   THEINVASIONWILLSTARTONTHEFIRSTOFJANUARY
Encrypted: RBPDHPHOQTNRBITMFODYPSAOSIAOBTOPDHTDCVI
Decrypted: THEINVASIONWILLSTARTONTHEFIRSTOFIANUARY

─────────────────────────
Using Polybius square:
  1 2 3 4 5 6
1 A B C D E F
2 G H I J K L
3 M N O P Q R
4 S T U V W X
5 Y Z 0 1 2 3
6 4 5 6 7 8 9

Message:   THEINVASIONWILLSTARTONTHEFIRSTOFJANUARY
Encrypted: TBPDIPHJSPOTAIVMGPCZKNSCN09BFIHK64I7BM4
Decrypted: THEINVASIONWILLSTARTONTHEFIRSTOFJANUARY

─────────────────────────

Perl

use v5.36;
use builtin <indexed floor>;
use experimental qw(builtin for_list);
use List::Util 'max';

sub table ($c, @V) { my $t = $c * (my $w = 2 + length max map { length } @V); ( sprintf( ('%'.$w.'s')x@V, @V) ) =~ s/.{1,$t}\K/\n/gr }

sub polybius ($text) {
    my %p;
    my $n = floor sqrt length $text;
    for my($k,$v) (indexed split '', $text) {
        $p{$v} = join ' ', $k%$n, int $k/$n
    }
    %p;
}

sub encrypt ($text, %P) {
    my(%I, @c, $encrypted);
    for my($k,$v) (%P) { $I{$v} = $k }
    for my ($n,$char) (indexed split '', ($text =~ s/\s//gr)) {
        for my($m,$i) (indexed split ' ', $P{$char}) { $c[$m][$n] = $i }
    }
    for my($i,$j) ($c[1]->@*, $c[0]->@*) { $encrypted .= $I{"$j $i"} }
    $encrypted
}

sub decrypt ($text, %P) {
    my($decrypted, $l, %I, @c) = ('', length($text));
    for my($k,$v) (%P) { $I{$v} = $k }
    for (split '', $text) {
        for my($i,$j) (split ' ', $P{$_}) { unshift @c, $i, $j }
    }
    substr $decrypted, 0, 0, $I{ "$c[$_] $c[$_+$l]" } for 0 .. $l-1;
    $decrypted;
}

for my($polybius,$message) (
  join('','A'..'Z') =~ s/J//r,                 'ATTACK AT DAWN',
  'BGWKZQPNDSIOAXEFCLUMTHYVR',                 'FLEE AT ONCE',
  join('','_.', 'A'..'Z', 'a'..'z', '0'..'9'), 'The_invasion_will_start_on_the_first_of_January_2023.',
  ) {
    my %Ptable = polybius $polybius;
    say "\nUsing polybius:\n" . table sqrt length $polybius, split '', $polybius;
    say 'Message   : ' .  $message;
    say 'Encrypted : ' .  (my $encrypted = encrypt $message, %Ptable);
    say 'Decrypted : ' .  decrypt $encrypted, %Ptable;
}
Output:
Using polybius:
  A  B  C  D  E
  F  G  H  I  K
  L  M  N  O  P
  Q  R  S  T  U
  V  W  X  Y  Z

Message   : ATTACK AT DAWN
Encrypted : DQBDAXDQPDQH
Decrypted : ATTACKATDAWN

Using polybius:
  B  G  W  K  Z
  Q  P  N  D  S
  I  O  A  X  E
  F  C  L  U  M
  T  H  Y  V  R

Message   : FLEE AT ONCE
Encrypted : UAEOLWRINS
Decrypted : FLEEATONCE

Using polybius:
  _  .  A  B  C  D  E  F
  G  H  I  J  K  L  M  N
  O  P  Q  R  S  T  U  V
  W  X  Y  Z  a  b  c  d
  e  f  g  h  i  j  k  l
  m  n  o  p  q  r  s  t
  u  v  w  x  y  z  0  1
  2  3  4  5  6  7  8  9

Message   : The_invasion_will_start_on_the_first_of_January_2023.
Encrypted : SejxqrEierbmrDiCjrDeJsbu89DWCHkgGS9E6tAG5_Ks2PBfCq_uH
Decrypted : The_invasion_will_start_on_the_first_of_January_2023.

Phix

with javascript_semantics
enum encrypt,decrypt
function bifid(string msg, polybius, sequence p, integer n, ed)
    string res = ""
    sequence dx = {}, dy = {}, dxy
    for i=1 to length(msg) do
        integer k = find(msg[i],polybius)-1
        dx &= floor(k/n)+1
        dy &= remainder(k,n)+1
    end for
    if ed=encrypt then
        dxy = split_by(dx&dy,2)
    else
        -- "simply reversing these steps" - yeah, right...
        dxy = columnize(split_by(flatten(columnize({dx,dy})),length(dx)))
    end if
    for i=1 to length(dxy) do
        integer {x,y} = dxy[i]
        res &= p[x][y]
    end for
    return res
end function

procedure test(string msg, polybius)
    integer n = sqrt(length(polybius))
    if n=5 then msg = substitute(upper(msg),"J","I") end if
    sequence p = split_by(polybius,n)
    string enc = bifid(msg,polybius,p,n,encrypt),
           dec = bifid(enc,polybius,p,n,decrypt)
    printf(1,"For %dx%d polybius %s\n\n",{n,n,join(p,"\n                 ")})
    printf(1,"Message  : %s\nEncrypted: %s\nDecrypted: %s\n\n",{msg,enc,dec})
end procedure
    
constant polybii = {tagstart('A','J'-'A')&tagstart('K','Z'-'J'),
                    "BGWKZQPNDSIOAXEFCLUMTHYVR",
                    " ."&tagstart('A',26)&tagstart('a',26)&tagstart('0',10)},
         messages = {"ATTACKATDAWN","FLEEATONCE",
                     "The invasion will start on the first of January 2023."}
for t=1 to 3 do test(messages[t],polybii[t]) end for
Output:
For 5x5 polybius ABCDE
                 FGHIK
                 LMNOP
                 QRSTU
                 VWXYZ

Message  : ATTACKATDAWN
Encrypted: DQBDAXDQPDQH
Decrypted: ATTACKATDAWN

For 5x5 polybius BGWKZ
                 QPNDS
                 IOAXE
                 FCLUM
                 THYVR

Message  : FLEEATONCE
Encrypted: UAEOLWRINS
Decrypted: FLEEATONCE

For 8x8 polybius  .ABCDEF
                 GHIJKLMN
                 OPQRSTUV
                 WXYZabcd
                 efghijkl
                 mnopqrst
                 uvwxyz01
                 23456789

Message  : The invasion will start on the first of January 2023.
Encrypted: SejxqrEierbmrDiCjrDeJsbu89DWCHkgGS9E6tAG5 Ks2PBfCq uH
Decrypted: The invasion will start on the first of January 2023.

Python

"""Bifid cipher. Requires Python >=3.7."""
import math
import pprint
import string

from itertools import chain
from itertools import zip_longest

from typing import Dict
from typing import Iterable
from typing import Iterator
from typing import Tuple
from typing import TypeVar


T = TypeVar("T")


def group(it: Iterable[T], n: int) -> Iterator[Tuple[T, ...]]:
    """Return the input iterable split in to `n` equal chunks, padded with `None`."""
    return zip_longest(*[iter(it)] * n)


Square = Tuple[Tuple[str, ...], ...]


def polybius_square(alphabet: str) -> Square:
    """Return the given alphabet as a tuple of tuples, representing a Polybius square."""
    return tuple(group(alphabet, math.ceil(math.sqrt(len(alphabet)))))


def polybius_map(square: Square) -> Dict[str, Tuple[int, int]]:
    """Return a reverse lookup for the given Polybius square."""
    return {
        square[i][j]: (i + 1, j + 1)
        for i in range(len(square))
        for j in range(len(square))
    }


def encrypt(message: str, square: Square) -> str:
    """Encrypt a plaintext message using a bifid cipher with the given Polybius square."""
    _map = polybius_map(square)
    return "".join(
        square[x - 1][y - 1]
        for x, y in group(
            chain.from_iterable(zip(*(_map[c] for c in message if c in _map))),
            2,
        )
    )


def decrypt(message: str, square: Square) -> str:
    """Decrypt a ciphertext message using a bifid cipher with the given Polybius square."""
    _map = polybius_map(square)
    return "".join(
        square[x - 1][y - 1]
        for x, y in zip(
            *group(
                chain.from_iterable((_map[c] for c in message if c in _map)),
                len(message),
            )
        )
    )


def normalize(message: str) -> str:
    """Normalize a message for the typical Polybius square."""
    return message.upper().replace("J", "I")


TYPICAL_POLYBIUS_SQUARE = polybius_square(
    alphabet="".join(c for c in string.ascii_uppercase if c != "J")
)


EXAMPLE_POLYBIUS_SQUARE = polybius_square(
    alphabet="BGWKZQPNDSIOAXEFCLUMTHYVR",
)


def main() -> None:
    test_cases = [
        ("ATTACKATDAWN", TYPICAL_POLYBIUS_SQUARE),  # 1
        ("FLEEATONCE", EXAMPLE_POLYBIUS_SQUARE),  # 2
        ("FLEEATONCE", TYPICAL_POLYBIUS_SQUARE),  # 3
        (
            normalize("The invasion will start on the first of January"),
            polybius_square(alphabet="PLAYFIREXMBCDGHKNOQSTUVWZ"),
        ),
        (
            "The invasion will start on the first of January".upper(),
            polybius_square(alphabet=string.ascii_uppercase + string.digits),
        ),
    ]

    for message, square in test_cases:
        pprint.pprint(square)
        print("Message  :", message)
        print("Encrypted:", encrypt(message, square))
        print("Decrypted:", decrypt(encrypt(message, square), square))
        print("")


if __name__ == "__main__":
    main()
Output:
(('A', 'B', 'C', 'D', 'E'),
 ('F', 'G', 'H', 'I', 'K'),
 ('L', 'M', 'N', 'O', 'P'),
 ('Q', 'R', 'S', 'T', 'U'),
 ('V', 'W', 'X', 'Y', 'Z'))
Message  : ATTACKATDAWN
Encrypted: DQBDAXDQPDQH
Decrypted: ATTACKATDAWN

(('B', 'G', 'W', 'K', 'Z'),
 ('Q', 'P', 'N', 'D', 'S'),
 ('I', 'O', 'A', 'X', 'E'),
 ('F', 'C', 'L', 'U', 'M'),
 ('T', 'H', 'Y', 'V', 'R'))
Message  : FLEEATONCE
Encrypted: UAEOLWRINS
Decrypted: FLEEATONCE

(('A', 'B', 'C', 'D', 'E'),
 ('F', 'G', 'H', 'I', 'K'),
 ('L', 'M', 'N', 'O', 'P'),
 ('Q', 'R', 'S', 'T', 'U'),
 ('V', 'W', 'X', 'Y', 'Z'))
Message  : FLEEATONCE
Encrypted: HADNAAZDSP
Decrypted: FLEEATONCE

(('P', 'L', 'A', 'Y', 'F'),
 ('I', 'R', 'E', 'X', 'M'),
 ('B', 'C', 'D', 'G', 'H'),
 ('K', 'N', 'O', 'Q', 'S'),
 ('T', 'U', 'V', 'W', 'Z'))
Message  : THE INVASION WILL START ON THE FIRST OF IANUARY
Encrypted: VRSYXSIYTMQVIRSKISLPVLDTCKRTCAIVTMATCEX
Decrypted: THEINVASIONWILLSTARTONTHEFIRSTOFIANUARY

(('A', 'B', 'C', 'D', 'E', 'F'),
 ('G', 'H', 'I', 'J', 'K', 'L'),
 ('M', 'N', 'O', 'P', 'Q', 'R'),
 ('S', 'T', 'U', 'V', 'W', 'X'),
 ('Y', 'Z', '0', '1', '2', '3'),
 ('4', '5', '6', '7', '8', '9'))
Message  : THE INVASION WILL START ON THE FIRST OF JANUARY
Encrypted: TBPDIPHJSPOTAIVMGPCZKNSCN09BFIHK64I7BM4
Decrypted: THEINVASIONWILLSTARTONTHEFIRSTOFJANUARY

Raku

Technically incorrect as the third part doesn't "Convert ... to upper case and ignore spaces".

sub polybius ($text) {
    my $n = $text.chars.sqrt.narrow;
    $text.comb.kv.map: { $^v => ($^k % $n, $k div $n).join: ' ' }
}

sub encrypt ($message, %poly) {
    %poly.invert.hash{(flat reverse [Z] %poly{$message.comb}».words).batch(2)».reverse».join: ' '}.join
}

sub decrypt ($message, %poly) {
   %poly.invert.hash{reverse [Z] (reverse flat %poly{$message.comb}».words».reverse).batch($message.chars)}.join
}


for 'ABCDEFGHIKLMNOPQRSTUVWXYZ', 'ATTACKATDAWN',
    'BGWKZQPNDSIOAXEFCLUMTHYVR', 'FLEEATONCE',
    (flat '_', '.', 'A'..'Z', 'a'..'z', 0..9).pick(*).join, 'The invasion will start on the first of January 2023.'.subst(/' '/, '_', :g)
 -> $polybius, $message {
    my %polybius = polybius $polybius;
    say "\nUsing polybius:\n\t" ~ $polybius.comb.batch($polybius.chars.sqrt.narrow).join: "\n\t";
    say "\n  Message : $message";
    say "Encrypted : " ~ my $encrypted = encrypt $message, %polybius;
    say "Decrypted : " ~ decrypt $encrypted, %polybius;
}
Output:
Using polybius:
	A B C D E
	F G H I K
	L M N O P
	Q R S T U
	V W X Y Z

  Message : ATTACKATDAWN
Encrypted : DQBDAXDQPDQH
Decrypted : ATTACKATDAWN

Using polybius:
	B G W K Z
	Q P N D S
	I O A X E
	F C L U M
	T H Y V R

  Message : FLEEATONCE
Encrypted : UAEOLWRINS
Decrypted : FLEEATONCE

Using polybius:
	H c F T N 5 f i
	_ U k R B Z V W
	3 G e v s w j x
	q S 2 8 y Q . O
	m 0 E d h D r u
	M p 7 Y 4 A 9 a
	t X l I 6 g b z
	J P n 1 K L C o

  Message : The_invasion_will_start_on_the_first_of_January_2023.
Encrypted : NGiw3okfXj4XoVE_NjWcLK4Sy28EivKo3aeNiti3N3z6HCHno6Fkf
Decrypted : The_invasion_will_start_on_the_first_of_January_2023.

Wren

One way of enabling all 26 letters to be encrypted uniquely would be to use a 6 x 6 Polybius square including the 10 digits. We could then encrypt text using numerals as well.

However, the following just uses the standard version of the cipher.

import "./str" for Str
import "./seq" for Lst

class Bifid {
    static encrypt(polybius, message) {
        message = Str.upper(message).replace("J", "I")
        var rows = []
        var cols = []
        for (c in message) {
            var ix = polybius.indexOf(c)
            if (ix == -1) continue
            rows.add((ix/5).floor + 1)
            cols.add((ix%5) + 1)
        }
        var s = ""
        for (pair in Lst.chunks(rows + cols, 2)) {
            var ix = (pair[0] - 1) * 5 + pair[1] - 1
            s = s + polybius[ix]
        }
        return s
    }

    static decrypt(polybius, message) {
        var rows = []
        var cols = []
        for (c in message) {
            var ix = polybius.indexOf(c)
            rows.add((ix/5).floor + 1)
            cols.add((ix%5) + 1)
        }
        var lines = Lst.flatten(Lst.zip(rows, cols))
        var count = lines.count/2
        rows = lines[0...count]
        cols = lines[count..-1]
        var s = ""
        for (i in 0...count) {
            var ix = (rows[i] - 1) * 5 + cols[i] - 1
            s = s + polybius[ix]
        }
        return s
    }
}
var poly1 = "ABCDEFGHIKLMNOPQRSTUVWXYZ"
var poly2 = "BGWKZQPNDSIOAXEFCLUMTHYVR"
var poly3 = "PLAYFIREXMBCDGHKNOQSTUVWZ"
var polys = [poly1, poly2, poly2, poly3]
var msg1  = "ATTACKATDAWN"
var msg2  = "FLEEATONCE"
var msg3  = "The invasion will start on the first of January"
var msgs  = [msg1, msg2, msg1, msg3]
for (i in 0...msgs.count) {
    var encrypted = Bifid.encrypt(polys[i], msgs[i])
    var decrypted = Bifid.decrypt(polys[i], encrypted)
    System.print("Message   : %(msgs[i])")
    System.print("Encrypted : %(encrypted)")
    System.print("Decrypted : %(decrypted)")
    if (i < msgs.count-1) System.print()
}
Output:
Message   : ATTACKATDAWN
Encrypted : DQBDAXDQPDQH
Decrypted : ATTACKATDAWN

Message   : FLEEATONCE
Encrypted : UAEOLWRINS
Decrypted : FLEEATONCE

Message   : ATTACKATDAWN
Encrypted : EYFENGIWDILA
Decrypted : ATTACKATDAWN

Message   : The invasion will start on the first of January
Encrypted : VRSYXSIYTMQVIRSKISLPVLDTCKRTCAIVTMATCEX
Decrypted : THEINVASIONWILLSTARTONTHEFIRSTOFIANUARY