ADFGVX cipher

From Rosetta Code
ADFGVX cipher is a draft programming task. It is not yet considered ready to be promoted as a complete task, for reasons that should be found in its talk page.
Description

The ADFGVX cipher was a manually applied field cipher used by the German Army during World War I. It was broken in 1918 by the French cryptanalyst Georges Painvin.

The workings of the cipher are described in the Wikipedia article, linked to above, and so will not be repeated here.

Task

Write routines, functions etc. in your language to:

1. Encrypt suitable plaintext and decrypt the resulting cipher text using the ADFGVX cipher algorithm given a Polybius square (see 2. below) and a suitable key. For this purpose suitable means text consisting solely of ASCII upper case letters or digits.

2. Create a 6 x 6 Polybius square using a random combination of the letters A to Z and the digits 0 to 9 and then display it.

3. Given the number of letters (between 7 and 12 say) to use, create a key by selecting a suitable word at random from unixdict.txt and then display it. The word selected should be such that none of its characters are repeated.

Use these routines to create a Polybius square and a 9 letter key.

These should then be used to encrypt the plaintext: ATTACKAT1200AM and decrypt the resulting cipher text. Display here the results of both operations.

Note

As it's unclear from the Wikipedia article how to handle a final row with fewer elements than the number of characters in the key, either of the methods mentioned in Columnar transposition may be used. In the case of the second method, it is also acceptable to fill any gaps after shuffling by moving elements to the left which makes decipherment harder.

11l[edit]

Translation of: Nim
V adfgvx = ‘ADFGVX’

F encrypt(plainText, polybius, key)
   V s = ‘’
   L(ch) plainText
      L(r) 6
         L(c) 6
            I polybius[r][c] == ch
               s ‘’= :adfgvx[r]‘’:adfgvx[c]

   DefaultDict[Char, String] cols
   L(ch) s
      cols[key[L.index % key.len]] ‘’= ch

   V result = ‘’
   L(k) sorted(cols.keys())
      I !result.empty
         result ‘’= ‘ ’
      result ‘’= cols[k]
   R result

F decrypt(cipherText, polybius, key)
   V skey = sorted(key)
   V cols = [‘’] * key.len
   V idx = 0
   L(col) cipherText.split(‘ ’)
      cols[key.findi(skey[idx])] = col
      idx++

   V s = ‘’
   L(i) 0 .< key.len
      L(col) cols
         I i < col.len
            s ‘’= col[i]

   V result = ‘’
   L(i) (0 .< s.len - 1).step(2)
      V r = :adfgvx.findi(s[i])
      V c = :adfgvx.findi(s[i + 1])
      result ‘’= polybius[r][c]
   R result

V polybius = [[Char("\0")] * 6] * 6
V alphabet = ‘ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789’
random:shuffle(&alphabet)
L(r) 6
   L(c) 6
      polybius[r][c] = alphabet[6 * r + c]

print("6 x 6 Polybius square:\n")
print(‘  | A D F G V X’)
print(‘---------------’)
L(row) polybius
   print(adfgvx[L.index]‘ | ’row.join(‘ ’))

V words = File(‘unixdict.txt’).read().split("\n").filter(w -> w.len == 9 & w.len == Set(Array(w)).len)
V key = random:choice(words).uppercase()
print("\nThe key is "key)

V PlainText = ‘ATTACKAT1200AM’
print("\nPlaintext : "PlainText)

V cipherText = encrypt(PlainText, polybius, key)
print("\nEncrypted : "cipherText)

V plainText = decrypt(cipherText, polybius, key)
print("\nDecrypted : "plainText)
Output:
6 x 6 Polybius square:

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

The key is EXCURSION

Plaintext : ATTACKAT1200AM

Encrypted : XFD GFVD GDG DGF DVD XDD DXV DGV DFF

Decrypted : ATTACKAT1200AM

F#[edit]

// ADFGVX cipher. Nigel Galloway: August 23rd., 2021
let polybus=let n=[|yield! {'A'..'Z'}; yield! {'0'..'9'}|] in MathNet.Numerics.Combinatorics.GeneratePermutation 36|>Array.map(fun g->n.[g]),[|'A';'D';'F';'G';'V';'X'|]
let printPolybus(a,g)=printf "    "; g|>Array.iter(printf "%c  "); printfn ""; printfn "    ----------------"
                      g|>Array.iteri(fun n g->printf " %c|" g; [0..5]|>List.iter(fun g->printf " %c " (Array.item(n*6+g) a)); printfn "")
let c2p n g=let g=(fst>>(Array.findIndex((=) g))) n in let (n:char[])=(snd n) in [|n.[g/n.Length];n.[g%n.Length]|]
let p2c n (g:char[])=Array.item(let n=snd n in (Array.findIndex((=)g.[0]) n)*n.Length+(Array.findIndex((=)g.[1]) n))(fst n)
let fN(g:string)=let e,d=let n=g|>Seq.sort|>List.ofSeq in (g|>Seq.mapi(fun i l->(List.findIndex((=)l)n)-i)|>Array.ofSeq,n|>Seq.mapi(fun i l->(Seq.findIndex((=)l)g)-i)|>Array.ofSeq)
                 (fun i->i+(e.[i%g.Length])),(fun i->i+(d.[i%g.Length]))
let ADFGVX n (g:string)=let pE,pD=fN g
                        (fun(s:string)->let a,b=s|>Seq.collect(c2p n)|>Array.ofSeq|>Array.splitAt(2*s.Length-(2*s.Length)%g.Length)
                                        Array.append(Array.permute(pE) a)(Array.permute(fst(fN(g.[..b.Length-1]))) b)|>System.String),
                        (fun(s:string)->let a,b=s.ToCharArray()|>Array.splitAt(s.Length-(s.Length)%g.Length)
                                        Array.append(Array.permute(pD) a)(Array.permute(snd(fN(g.[..b.Length-1]))) b)|>Array.chunkBySize 2|>Array.map(p2c n)|>System.String)

printPolybus polybus
let encrypt,decrypt=ADFGVX polybus "nigel" //Using "nigel" as the key no hacker will guess that!
let n=encrypt "ATTACKAT1200AM" in printfn $"\nATTACKAT1200AM encrypted is %s{n} which decrypted is %s{decrypt n}"
Output:
    A  D  F  G  V  X
    ----------------
 A| 6  U  9  B  2  H
 D| C  G  O  N  Y  R
 F| T  8  D  E  V  K
 G| 1  Q  X  P  L  W
 V| Z  0  I  A  J  S
 X| 7  4  3  M  F  5

ATTACKAT1200AM encrypted is AFGFVDGVAAGVXFFAAGVADVDVVGXG which decrypted is ATTACKAT1200AM

Go[edit]

Translation of: Wren
package main

import (
    "bytes"
    "fmt"
    "io/ioutil"
    "log"
    "math/rand"
    "sort"
    "strings"
    "time"
)

var adfgvx = "ADFGVX"
var alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"

func distinct(bs []byte) []byte {
    var u []byte
    for _, b := range bs {
        if !bytes.Contains(u, []byte{b}) {
            u = append(u, b)
        }
    }
    return u
}

func allAsciiAlphaNum(word []byte) bool {
    for _, b := range word {
        if !((b >= 48 && b <= 57) || (b >= 65 && b <= 90) || (b >= 97 && b <= 122)) {
            return false
        }
    }
    return true
}

func orderKey(key string) []int {
    temp := make([][2]byte, len(key))
    for i := 0; i < len(key); i++ {
        temp[i] = [2]byte{key[i], byte(i)}
    }
    sort.Slice(temp, func(i, j int) bool { return temp[i][0] < temp[j][0] })
    res := make([]int, len(key))
    for i := 0; i < len(key); i++ {
        res[i] = int(temp[i][1])
    }
    return res
}

func createPolybius() []string {
    temp := []byte(alphabet)
    rand.Shuffle(36, func(i, j int) {
        temp[i], temp[j] = temp[j], temp[i]
    })
    alphabet = string(temp)
    fmt.Println("6 x 6 Polybius square:\n")
    fmt.Println("  | A D F G V X")
    fmt.Println("---------------")
    p := make([]string, 6)
    for i := 0; i < 6; i++ {
        fmt.Printf("%c | ", adfgvx[i])
        p[i] = alphabet[6*i : 6*(i+1)]
        for _, c := range p[i] {
            fmt.Printf("%c ", c)
        }
        fmt.Println()
    }
    return p
}

func createKey(n int) string {
    if n < 7 || n > 12 {
        log.Fatal("Key should be within 7 and 12 letters long.")
    }
    bs, err := ioutil.ReadFile("unixdict.txt")
    if err != nil {
        log.Fatal("Error reading file")
    }
    words := bytes.Split(bs, []byte{'\n'})
    var candidates [][]byte
    for _, word := range words {
        if len(word) == n && len(distinct(word)) == n && allAsciiAlphaNum(word) {
            candidates = append(candidates, word)
        }
    }
    k := string(bytes.ToUpper(candidates[rand.Intn(len(candidates))]))
    fmt.Println("\nThe key is", k)
    return k
}

func encrypt(polybius []string, key, plainText string) string {
    temp := ""
outer:
    for _, ch := range []byte(plainText) {
        for r := 0; r <= 5; r++ {
            for c := 0; c <= 5; c++ {
                if polybius[r][c] == ch {
                    temp += fmt.Sprintf("%c%c", adfgvx[r], adfgvx[c])
                    continue outer
                }
            }
        }
    }
    colLen := len(temp) / len(key)
    // all columns need to be the same length
    if len(temp)%len(key) > 0 {
        colLen++
    }
    table := make([][]string, colLen)
    for i := 0; i < colLen; i++ {
        table[i] = make([]string, len(key))
    }
    for i := 0; i < len(temp); i++ {
        table[i/len(key)][i%len(key)] = string(temp[i])
    }
    order := orderKey(key)
    cols := make([][]string, len(key))
    for i := 0; i < len(key); i++ {
        cols[i] = make([]string, colLen)
        for j := 0; j < colLen; j++ {
            cols[i][j] = table[j][order[i]]
        }
    }
    res := make([]string, len(cols))
    for i := 0; i < len(cols); i++ {
        res[i] = strings.Join(cols[i], "")
    }
    return strings.Join(res, " ")
}

func decrypt(polybius []string, key, cipherText string) string {
    colStrs := strings.Split(cipherText, " ")
    // ensure all columns are same length
    maxColLen := 0
    for _, s := range colStrs {
        if len(s) > maxColLen {
            maxColLen = len(s)
        }
    }
    cols := make([][]string, len(colStrs))
    for i, s := range colStrs {
        var ls []string
        for _, c := range s {
            ls = append(ls, string(c))
        }
        if len(s) < maxColLen {
            cols[i] = make([]string, maxColLen)
            copy(cols[i], ls)
        } else {
            cols[i] = ls
        }
    }
    table := make([][]string, maxColLen)
    order := orderKey(key)
    for i := 0; i < maxColLen; i++ {
        table[i] = make([]string, len(key))
        for j := 0; j < len(key); j++ {
            table[i][order[j]] = cols[j][i]
        }
    }
    temp := ""
    for i := 0; i < len(table); i++ {
        temp += strings.Join(table[i], "")
    }
    plainText := ""
    for i := 0; i < len(temp); i += 2 {
        r := strings.IndexByte(adfgvx, temp[i])
        c := strings.IndexByte(adfgvx, temp[i+1])
        plainText = plainText + string(polybius[r][c])
    }
    return plainText
}

func main() {
    rand.Seed(time.Now().UnixNano())
    plainText := "ATTACKAT1200AM"
    polybius := createPolybius()
    key := createKey(9)
    fmt.Println("\nPlaintext :", plainText)
    cipherText := encrypt(polybius, key, plainText)
    fmt.Println("\nEncrypted :", cipherText)
    plainText2 := decrypt(polybius, key, cipherText)
    fmt.Println("\nDecrypted :", plainText2)
}
Output:

Sample run:

6 x 6 Polybius square:

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

The key is EUCHARIST

Plaintext : ATTACKAT1200AM

Encrypted : FDG FGG FVDV FFX FFF FFX DDD XAF DGG

Decrypted : ATTACKAT1200AM

J[edit]

Implementation:
polybius=: {{6 6$8 u:({~?~&#)(48+i.10),65+i.26}}
lenword=: {{ ;({~ ?@#)(#~ (-:~.)@>)(#~ y=#@>)cutLF fread'unixdict.txt'}}
ADFGVX=: {{ deb,' ',.n/:~|:(-#n)]\'ADFGVX'{~,($m)#:(,m)i.y([-.-.),m }}
XVGFDA=: {{ (,m){~($m)#.'ADFGVX'i._2]\deb,|:(>cut y)/:/:n }}
Example:
   echo W=: lenword 9
roughcast
   echo P=: polybius ''
PV5M6Q
KR0391
4ZS7LA
FUT28E
GXOBYW
ICJDNH
   echo E=: P ADFGVX W 'ATTACKAT1200AM'
FFF FGF FFF GXD XDG FDGG XDX XXA GAD
   echo D=: P XVGFDA W E
ATTACKAT1200AM

That said, note that we could also eliminate spaces from the encrypted text, as they are recoverable if we have the key word.

spaces=: {{deb y#inv~,0,.(/:n){*|:_9]\>:i.#y=.y-.' '}}

   echo S=: E-.' '
FFFFGFFFFGXDXDGFDGGXDXXXAGAD
   P spaces W S
FFF FGF FFF GXD XDG FDGG XDX XXA GAD

(Technically, we do not need the Polybius square to recover the spaces, but it's passed as an argument here for symmetry.)

Julia[edit]

"""
    The ADFGVX cipher.
    See also eg. https://www.nku.edu/~christensen/092hnr304%20ADFGVX.pdf
"""

using Random

""" The WWI German ADFGVX cipher. """
struct ADFGVX
    polybius::Vector{Char}
    pdim::Int
    key::Vector{Char}
    keylen::Int
    alphabet::Vector{Char}
    encode::Dict{Char, Vector{Char}}
    decode::Dict{Vector{Char}, Char}
end

""" ADFGVX constructor, takes 2 strings, option for third string if polybius len != 36 """
function ADFGVX(s, k, alph = "ADFGVX")
    poly = collect(uppercase(s))
    pdim = isqrt(length(poly))
    al = collect(uppercase(alph))
    enco::Dict = Dict([(poly[(i - 1) * pdim + j] => [al[i], al[j]])
        for i in 1:pdim, j in 1:pdim])
    deco = Dict(last(p) => first(p) for p in enco)
    @assert pdim^2 == length(poly) && pdim == length(al)
    return ADFGVX(poly, pdim, collect(uppercase(k)), length(k), al, enco, deco)
end

""" Encrypt with the ADFGVX cipher. """
function encrypt(s::String, k::ADFGVX)
    chars = reduce(vcat, [k.encode[c] for c in
        filter(c -> c in k.polybius, collect(uppercase(s)))])
    colvecs = [lett => chars[i:k.keylen:length(chars)] for (i, lett) in enumerate(k.key)]
    sort!(colvecs, lt = (x, y) -> first(x) < first(y))
    return String(mapreduce(p -> last(p), vcat, colvecs))
end

""" Decrypt with the ADFGVX cipher. Does not depend on spacing of encoded text """
function decrypt(s::String, k::ADFGVX)
    chars = filter(c -> c in k.alphabet, collect(uppercase(s)))
    sortedkey = sort(collect(k.key))
    order = [findfirst(c -> c == ch, k.key) for ch in sortedkey]
    originalorder = [findfirst(c -> c == ch, sortedkey) for ch in k.key]
    a, b = divrem(length(chars), k.keylen)
    strides = [a + (b >= i ? 1 : 0) for i in order]           # shuffled column lengths
    starts = accumulate(+, strides[begin:end-1], init=1)      # shuffled starts of columns
    pushfirst!(starts, 1)                                     # starting index
    ends = [starts[i] + strides[i] - 1 for i in 1:k.keylen]   # shuffled ends of columns
    cols = [chars[starts[i]:ends[i]] for i in originalorder]  # get reordered columns
    pairs, nrows = Char[], (length(chars) - 1) ÷ k.keylen + 1 # recover the rows
    for i in 1:nrows, j in 1:k.keylen
        (i - 1) * k.keylen + j > length(chars) && break
        push!(pairs, cols[j][i])
    end
    return String([k.decode[[pairs[i], pairs[i + 1]]] for i in 1:2:length(pairs)-1])
end

const POLYBIUS = String(shuffle(collect("ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789")))
const KEY = read("unixdict.txt", String) |>
    v -> split(v, r"\s+") |>
    v -> filter(w -> (n = length(w); n == 9 && n == length(unique(collect(w)))), v) |>
    shuffle |> first |> uppercase
const SECRETS, message = ADFGVX(POLYBIUS, KEY), "ATTACKAT1200AM"
println("Polybius: $POLYBIUS, Key: $KEY")
println("Message: $message")
encoded = encrypt(message, SECRETS)
decoded = decrypt(encoded, SECRETS)
println("Encoded: $encoded")
println("Decoded: $decoded")
Output:
Polybius: L4VZJIB8OXGFM1H3CTNKU9PE75WQ2DAYRS06, Key: SUNFLOWER
Message: ATTACKAT1200AM
Encoded: AFAXXVFAXFDXXFVFDFXVVAAGVXXX
Decoded: ATTACKAT1200AM

Nim[edit]

Translation of: Wren

It started as a translation, but actually we use a different method, better suited to Nim, to encrypt and decrypt. And there are many other differences. Output is similar though.

import algorithm, random, sequtils, strutils, sugar, tables

const Adfgvx = "ADFGVX"

type PolybiusSquare = array[6, array[6, char]]


iterator items(p: PolybiusSquare): (int, int, char) =
  ## Yield Polybius square characters preceded by row and column numbers.
  for r in 0..5:
    for c in 0..5:
      yield (r, c, p[r][c])


proc initPolybiusSquare(): PolybiusSquare =
  ## Initialize a 6x6 Polybius square.
  var alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
  alphabet.shuffle()
  for r in 0..5:
    for c in 0..5:
      result[r][c] = alphabet[6 * r + c]


proc createKey(n: Positive): string =
  ## Create a key using a word from "unixdict.txt".
  doAssert n in 7..12, "Key should be within 7 and 12 letters long."
  let candidates = collect(newSeq):
                   for word in "unixdict.txt".lines:
                     if word.len == n and
                        word.deduplicate().len == n and
                        word.allCharsInSet(Letters + Digits): word
  result = candidates[rand(candidates.high)].toUpperAscii


func encrypt(plainText: string; polybius: PolybiusSquare; key: string): string =
  ## Encrypt "plaintext" using the given Polybius square and the given key.

  # Replace characters by row+column letters.
  var str: string
  for ch in plainText:
    for (r, c, val) in polybius:
      if val == ch:
        str.add Adfgvx[r] & Adfgvx[c]

  # Build ordered table of columns and sort it by key value.
  var cols: OrderedTable[char, string]
  for i, ch in str:
    let tkey = key[i mod key.len]
    cols.mgetOrPut(tkey, "").add ch
  cols.sort(cmp)

  # Build cipher text from sorted column table values.
  for s in cols.values:
    result.addSep(" ")
    result.add s


func decrypt(cipherText: string; polybius: PolybiusSquare; key: string): string =
  ## Decrypt "cipherText" using the given Polybius square and the given key.

  # Build list of columns.
  let skey = sorted(key)
  var cols = newSeq[string](key.len)
  var idx = 0
  for col in cipherText.split(' '):
    cols[key.find(skey[idx])] = col
    inc idx

  # Build string of row+column values.
  var str: string
  for i in 0..key.high:
    for col in cols:
      if i < col.len: str.add col[i]

  # Build plain text from row+column values.
  for i in countup(0, str.len - 2, 2):
    let r = Adfgvx.find(str[i])
    let c = Adfgvx.find(str[i+1])
    result.add polybius[r][c]


randomize()

var polybius = initPolybiusSquare()
echo "6 x 6 Polybius square:\n"
echo "  | A D F G V X"
echo "---------------"
for i, row in polybius:
  echo Adfgvx[i], " | ", row.join(" ")

let key = createKey(9)
echo "\nThe key is ", key

const PlainText = "ATTACKAT1200AM"
echo "\nPlaintext : ", PlainText

let cipherText = PlainText.encrypt(polybius, key)
echo "\nEncrypted : ", cipherText

let plainText = cipherText.decrypt(polybius, key)
echo "\nDecrypted : ", plainText
Output:
6 x 6 Polybius square:

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

The key is PHAGOCYTE

Plaintext : ATTACKAT1200AM

Encrypted : XXX GXA ADD GVA AGD XAX VFGD AAA VGV

Decrypted : ATTACKAT1200AM

Perl[edit]

#!/usr/bin/perl

use strict; # https://rosettacode.org/wiki/ADFGVX_cipher
use warnings;
use List::Util qw( shuffle );

my $plaintext = 'ATTACKAT1200AM';
my $keysize = 9;

my $polybius = <<END;
  | A D F G V X
--+------------
A | x x x x x x
D | x x x x x x
F | x x x x x x
G | x x x x x x
V | x x x x x x
X | x x x x x x
END
$polybius =~ s/x/$_/ for my @letters = shuffle "A" .. 'Z' , 0 .. 9;
print "Polybius square =\n\n$polybius\n";
my %char2pair;
@char2pair{ @letters } = glob '{A,D,F,G,V,X}' x 2; # map chars to pairs
my %pair2char = reverse %char2pair;                # map pairs to chars
my ($keyword) = shuffle grep !/(.).*\1/,
  do { local (@ARGV, $/) = 'unixdict.txt'; <> =~ /^.{$keysize}$/gm };
my ($n, @deorder) = 0;
my @reorder = map /.(.+)/, sort map $_ . $n++, split //, $keyword;
@deorder[@reorder] = 0 .. $#reorder;
print "  keyword = $keyword\n\nplaintext = $plaintext\n\n";

my $encoded = encode( $plaintext, \%char2pair, \@reorder );
print "  encoded = $encoded\n\n";

my $decoded = decode( $encoded, \%pair2char, \@deorder );
print "  decoded = $decoded\n";

sub encode
  {
  my ($plain, $c2p, $order) = @_;
  my $len = @$order;
  join ' ', (transpose( $plain =~ s/./$c2p->{$&}/gr =~ /.{1,$len}/g ))[@$order];
  }

sub decode
  {
  my ($encoded, $p2c, $order) = @_;
  (join '', transpose((split ' ', $encoded)[@$order])) =~ s/../$p2c->{$&}/gr;
  }

sub transpose { map join('', map {s/.// ? $& : ''} @_), 1 .. length $_[0] }
Output:
Polybius square =

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

  keyword = benchmark

plaintext = ATTACKAT1200AM

  encoded = GDG GDVF DGG AXD FAX DAD DFG FAX ADA

  decoded = ATTACKAT1200AM

Phix[edit]

Translation of: Wren
We can make some nice use of the standard builtin routines here, with only a modest amount of whitespace cleanup.
with javascript_semantics
constant ADFGVX = "ADFGVX",
         ALEPH = tagset('Z','A')&tagset('9','0')

function create_polybius()
    string aleph = shuffle(ALEPH)
--  string aleph = "U1CNHFEM4RSGPI8965X2ZB7KA3YVOD0WQTJL"   -- Nim
--  string aleph = "T71VB5HYG2JKIQM8REOPDUNCZ063FXAW9S4L"   -- Wren
--  string aleph = "NA1C3H8TB2OME5WRPD4F6G7I9J0KLQSUVXYZ"   -- wp
    sequence tmp = split(join_by(aleph,1,6," "),'\n')
    printf(1,"6 x 6 Polybius square:\n")
    printf(1,"  | A D F G V X\n")
    printf(1,"---------------\n")
    for i=1 to length(tmp) do
        printf(1,"%s | %s\n",{ADFGVX[i],tmp[i]})
    end for
    return aleph
end function

function lnua(string word, integer n)
    return length(word)==n 
       and length(unique(word))==n
       and length(filter(word,"in",ALEPH))==n
end function

function create_key(integer n)
    assert(n>=7 and n<=12)
    sequence candidates = filter(upper(unix_dict()),lnua,n)
    string res = candidates[rand(length(candidates))]
--  string res = "PHAGOCYTE" -- Nim
--  string res = "SUNFLOWER" -- Wren
--  string res = "PRIVACY" -- wp
    printf(1,"\nThe key is %s\n",{res})
    return res
end function

function encrypt(string polybius, key, plaintext)
    integer l = length(key)
    sequence tags = custom_sort(key,tagset(l)),
             res = ""
    for i=1 to length(plaintext) do
        integer k = find(plaintext[i],polybius)
        if k then   -- (simply ignore any non-alphanum)
            res &= ADFGVX[floor((k-1)/6)+1]&
                   ADFGVX[remainder((k-1),6)+1]
        end if
    end for
    res = substitute(join(columnize(split_by(res,l),tags,' ')),"  "," ")
    return res
end function

function decrypt(string polybius, key, encrypted)
    integer l = length(key)
    sequence tags = custom_sort(key,tagset(l)),
             tmp = columnize(split(encrypted,' '),{},' ')
    tmp = trim(join(apply(true,extract,{tmp,{tags},true}),""))
    string plaintext = ""
    for i=1 to length(tmp) by 2 do
        integer r = find(tmp[i],ADFGVX)-1,
                c = find(tmp[i+1],ADFGVX)
        plaintext &= polybius[r*6+c]
    end for
    return plaintext   
end function

string polybius = create_polybius(),
            key = create_key(9),
       plaintext = "ATTACKAT1200AM",
       encrypted = encrypt(polybius,key,plaintext),
       decrypted = decrypt(polybius,key,encrypted)
printf(1,"\nPlainText : %s\n\nEncrypted : %s\n\nDecrypted : %s\n",
           {plaintext,        encrypted,        decrypted})

Output matches Wren/Nim/wp when the appropriate lines are uncommented.

Python[edit]

This version and the Julia version do not reveal the key length by preserving spaces between columns, which completely removes column information, not just hiding which columns are first in the encoding. According to historical sources the original encrypted text was spaced in 5 character blocks regardless of message and key length, which means that decryption should not rely on spacing.

"""
    The ADFGVX cipher implemented as a Python class 
    See also eg. https://www.nku.edu/~christensen/092hnr304%20ADFGVX.pdf
"""

from random import shuffle, choice
from itertools import product, accumulate
from numpy import floor, sqrt

class ADFGVX:
    """ The WWI German ADFGVX cipher. """
    def __init__(self, spoly, k, alph='ADFGVX'):
        self.polybius = list(spoly.upper())
        self.pdim = int(floor(sqrt(len(self.polybius))))
        self.key = list(k.upper())
        self.keylen = len(self.key)
        self.alphabet = list(alph)
        pairs = [p[0] + p[1] for p in product(self.alphabet, self.alphabet)]
        self.encode = dict(zip(self.polybius, pairs))
        self.decode = dict((v, k) for (k, v) in self.encode.items())

    def encrypt(self, msg):
        """ Encrypt with the ADFGVX cipher. """
        chars = list(''.join([self.encode[c] for c in msg.upper() if c in self.polybius]))
        colvecs = [(lett, chars[i:len(chars):self.keylen]) \
            for (i, lett) in enumerate(self.key)]
        colvecs.sort(key=lambda x: x[0])
        return ''.join([''.join(a[1]) for a in colvecs])

    def decrypt(self, cod):
        """ Decrypt with the ADFGVX cipher. Does not depend on spacing of encoded text """
        chars = [c for c in cod if c in self.alphabet]
        sortedkey = sorted(self.key)
        order = [self.key.index(ch) for ch in sortedkey]
        originalorder = [sortedkey.index(ch) for ch in self.key]
        base, extra = divmod(len(chars), self.keylen)
        strides = [base + (1 if extra > i else 0) for i in order]    # shuffled column lengths
        starts = list(accumulate(strides[:-1], lambda x, y: x + y))  # shuffled starts of columns
        starts = [0] + starts                                        # starting index
        ends = [starts[i] + strides[i] for i in range(self.keylen)]  # shuffled ends of columns
        cols = [chars[starts[i]:ends[i]] for i in originalorder]     # get reordered columns
        pairs = []                                                   # recover the rows
        for i in range((len(chars) - 1) // self.keylen + 1):
            for j in range(self.keylen):
                if i * self.keylen + j < len(chars):
                    pairs.append(cols[j][i])

        return ''.join([self.decode[pairs[i] + pairs[i + 1]] for i in range(0, len(pairs), 2)])


if __name__ == '__main__':
    PCHARS = list('ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789')
    shuffle(PCHARS)
    POLYBIUS = ''.join(PCHARS)
    with open('unixdict.txt') as fh:
        WORDS = [w for w in (fh.read()).split() \
            if len(w) == 9 and len(w) == len(set(list(w)))]
        KEY = choice(WORDS)

    SECRET, MESSAGE = ADFGVX(POLYBIUS, KEY), 'ATTACKAT1200AM'
    print(f'Polybius: {POLYBIUS}, key: {KEY}')
    print('Message: ', MESSAGE)
    ENCODED = SECRET.encrypt(MESSAGE)
    DECODED = SECRET.decrypt(ENCODED)
    print('Encoded: ', ENCODED)
    print('Decoded: ', DECODED)
Output:
Polybius: A9GKMF1DQRSBVX8Z0WTEJLOPY5U4CN2H76I3, key: volcanism
Message:  ATTACKAT1200AM
Encoded:  GAFAAVAAAGGFVAAAGVAAAADAAVXV
Decoded:  ATTACKAT1200AM

Raku[edit]

Slightly different results from every other entry so far. See discussion page for reasons. It is impossible to tell from casual observation which column comes first in the Raku example. In every other (so far), the sub group with 4 characters is the first column.

srand 123456; # For repeatability

my @header   = < A D F G V X >;
my $polybius = (flat 'A'..'Z', 0..9).pick(*).join;

my $key-length = 9;
my $key = uc 'unixdict.txt'.IO.words.grep( { (.chars == $key-length) && (+.comb.Set == +.comb) } ).roll;

my %cypher   = (@header X~ @header) Z=> $polybius.comb;

my $message = 'Attack at 1200AM';

use Terminal::Boxer;
say "Key: $key\n";
say "Polybius square:\n", ss-box :7col, :3cw, :indent("\t"), '', |@header, |(@header Z $polybius.comb.batch: 6);
say "Message to encode: $message";
say "\nEncoded: " ~ my $encoded = encode $message;
say "\nDecoded: " ~ decode $encoded;

sub encode ($text is copy) {
    $text = $text.uc.comb(/<[A..Z 0..9]>/).join;
    my @order = $key.comb.pairs.sort( *.value )».key;
    my @encode = %cypher.invert.hash{ $text.comb }.join.comb.batch($key-length).map: { [$_] };
    ((^$key-length).map: { @encode».[@order]».grep( *.defined )».[$_].grep( *.defined ).join }).Str;
}

sub decode ($text is copy) {
    my @text = $text.split(' ')».comb;
    my $chars = @text[0].chars;
    $_ = flat |$_, ' ' if .chars < $chars for @text;
    my @order = $key.comb.pairs.sort( *.value )».key.pairs.sort( *.value )».key;
    %cypher{ ( grep { /\w/ }, flat [Z] @order.map( { |@text.batch($key-length)».[$_] } ) ).batch(2)».join }.join;
}
Output:
Key: GHOSTLIKE

Polybius square:
	┌───┬───┬───┬───┬───┬───┬───┐
	│   │ A │ D │ F │ G │ V │ X │
	├───┼───┼───┼───┼───┼───┼───┤
	│ A │ H │ O │ S │ 5 │ 7 │ Q │
	├───┼───┼───┼───┼───┼───┼───┤
	│ D │ 0 │ I │ 6 │ J │ C │ V │
	├───┼───┼───┼───┼───┼───┼───┤
	│ F │ 4 │ 8 │ R │ X │ G │ A │
	├───┼───┼───┼───┼───┼───┼───┤
	│ G │ Y │ F │ P │ B │ Z │ 3 │
	├───┼───┼───┼───┼───┼───┼───┤
	│ V │ M │ W │ 9 │ D │ 1 │ N │
	├───┼───┼───┼───┼───┼───┼───┤
	│ X │ 2 │ E │ U │ T │ L │ K │
	└───┴───┴───┴───┴───┴───┴───┘

Message to encode: Attack at 1200AM

Encoded: DVVA FVX XXA FGF XVX GXA XXD GFA XXD

Decoded: ATTACKAT1200AM

Wren[edit]

Library: Wren-ioutil
Library: Wren-seq
Library: Wren-str
import "random" for Random
import "/ioutil" for FileUtil
import "/seq" for Lst
import "/str" for Char, Str

var rand = Random.new()
var adfgvx = "ADFGVX"
var alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789".toList

var createPolybius = Fn.new {
    rand.shuffle(alphabet)
    var p = Lst.chunks(alphabet, 6)
    System.print("6 x 6 Polybius square:\n")
    System.print("  | A D F G V X")
    System.print("---------------")
    for (i in 0...p.count) {
        System.write("%(adfgvx[i]) | ")
        System.print(p[i].join(" "))
    }
    return p
}

var createKey = Fn.new { |n|
    if (n < 7 || n > 12) Fiber.abort("Key should be within 7 and 12 letters long.")
    var candidates = FileUtil.readLines("unixdict.txt").where { |word| 
        return word.count == n && Lst.distinct(word.toList).count == n &&
               word.all { |ch| Char.isAsciiAlphaNum(ch) }
    }.toList
    var k = Str.upper(candidates[rand.int(candidates.count)])
    System.print("\nThe key is %(k)")
    return k
}

// helper function to sort the key into alphabetical order
// and return a list of the original indices of its letters.
var orderKey = Fn.new { |key|
    var temp = (0...key.count).map { |i| [key[i], i] }.toList
    temp.sort { |x, y| x[0].bytes[0] < y[0].bytes[0] }
    return temp.map { |e| e[1] }.toList
}

var encrypt = Fn.new { |polybius, key, plainText|
    var temp = ""
    for (ch in plainText) {
        var outer = false
        for (r in 0..5) {
            for (c in 0..5) {
                if (polybius[r][c] == ch) {
                    temp = temp + adfgvx[r] + adfgvx[c]
                    outer = true
                    break
                }
            }
            if (outer) break
        }
    }
    var colLen = (temp.count / key.count).floor
    // all columns need to be the same length
    if (temp.count % key.count > 0) colLen = colLen + 1
    var table = Lst.chunks(temp.toList, key.count)
    var lastLen = table[-1].count
    if (lastLen < key.count) table[-1] = table[-1] + ([""] * (key.count - lastLen))
    var order = orderKey.call(key)
    var cols = List.filled(key.count, null)
    for (i in 0...cols.count) {
        cols[i] = List.filled(colLen, null)
        for (j in 0...table.count) cols[i][j] = table[j][order[i]]
    }
    return cols.map { |col| col.join() }.join(" ")
}

var decrypt = Fn.new { |polybius, key, cipherText|
    var colStrs = cipherText.split(" ")
    // ensure all columns are same length
    var maxColLen = colStrs.reduce(0) { |max, col| max = (col.count > max) ? col.count : max }
    var cols = colStrs.map { |s| 
        return (s.count < maxColLen) ? s.toList + ([""] * (maxColLen - s.count)) : s.toList
    }.toList
    var table = List.filled(maxColLen, null)
    var order = orderKey.call(key)
    for (i in 0...maxColLen) {
        table[i] = List.filled(key.count, "")
        for (j in 0...key.count) table[i][order[j]] = cols[j][i]
    }
    var temp = table.map { |row| row.join("") }.join("")
    var plainText = ""
    var i = 0
    while (i < temp.count) {
        var r = adfgvx.indexOf(temp[i])
        var c = adfgvx.indexOf(temp[i+1])
        plainText = plainText + polybius[r][c]
        i = i + 2
    }
    return plainText   
}

var plainText = "ATTACKAT1200AM"
var polybius = createPolybius.call()
var key = createKey.call(9)
System.print("\nPlaintext : %(plainText)")
var cipherText = encrypt.call(polybius, key, plainText)
System.print("\nEncrypted : %(cipherText)")
var plainText2 = decrypt.call(polybius, key, cipherText)
System.print("\nDecrypted : %(plainText2)")
Output:

Sample run:

6 x 6 Polybius square:

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

The key is SUNFLOWER

Plaintext : ATTACKAT1200AM

Encrypted : AAA AXD AAV AXV AAD GFF XXDF ADG XAX

Decrypted : ATTACKAT1200AM

REXX[edit]

/* REXX */
cls
eol=x2c(0D0A) ; msg="ATTACKAT1200AM" 
keyword= upper('lifeguard') ; cyph= 'ADFGVX'
 
   s_sort= keyword ; new_key= ''  
   do while length(s_sort) > 0 
      nmax= 0
      do i=1 to length(s_sort)
         ch= substr(s_sort,i,1)
         num= c2d(ch)
         if num > nmax then do
            nmax= num
            max_i = i
            end
      end
      s_sort= delstr(s_sort,max_i,1)
      new_key= d2c(nmax)||new_key
   end        /* Alphabetical sorting */
   
   j=0 ; num_str= '' ; rnd_s= '' 
   do while j < 36
      num= random(0,35)
      if wordpos(num,num_str) = 0 then do
         j= j + 1
 	     num_str= num_str||num||' '
		 if num >= 10 then do
		    num= num - 10 + x2d(41)
            num= d2c(num)
		    end
		 rnd_s= rnd_s||num
      end
   end	  /* say 'Generated string: '||rnd_s||eol */
   
say 'Polybius square:'||eol
call tab cyph, rnd_s ,1 
say "Only characters from the '"|| msg||"'"||eol
t= translate(rnd_s,' ',msg)
_t= translate(rnd_s,' ',t)
call tab cyph, _t ,1

   len_c= length(cyph) ; cyph_T=''
   do i=1 to len_c
      ch_i= substr(cyph,i,1)
      do j=1 to len_c
         ch_j= substr(cyph,j,1)
         cyph_T= cyph_T||ch_i||ch_j||' '
      end
   end

   enc_msg= ''
   do i=1 to length(msg)
      ch= substr(msg,i,1)
       j= pos(ch,rnd_s,1)
      enc_msg= enc_msg||word(cyph_T,j) 
   end

say "Conversion by table: "||eol||eol||msg||" ==> "||enc_msg||eol
call tab keyword, enc_msg

   len= length(keyword)
   n_row= 0 ; column.= '' 
    do while enc_msg <> ''
       parse var enc_msg 1 s1 +(len) enc_msg
       n_row= n_row+1
       do m= 1 to len
          ch_m= substr(s1,m,1)
          column.m= column.m||ch_m 
          end 
    end

   s_lst= '' 
   do m= 1 to len
      ch= substr(new_key,m,1)
       i= pos(ch,keyword,1)
      w_i= column.i                 
      s_lst= s_lst||w_i||' '
   end
   
   row.= '' ; t_row= ''              
   do i=1 to len                              
      w_i= word(s_lst,i)                        
      do j=1 to n_row             
         row.j= row.j||substr(w_i,j,1)   
      end 
   end
   do j=1 to n_row; t_row= t_row||row.j; end

say "Sorted by columns:"||eol
call tab new_key, t_row 

say '~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~'
say 'Encrypted message: '||s_lst
say '~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~' 
   
exit
tab:
parse arg h, s, p  /* header, string, param */

   lh= length(h) ;
   s= h||copies('-',lh)||s ; ls= length(s)
   h=' -'||h

   t= ''  ; j= 1                   
   do i= 1 to ls by lh 
      row= substr(s,i,lh)
      r_ch= '' 
      do l=1 to lh
         ch= substr(row,l,1)
         r_ch= r_ch||ch||' '
      end
      row= r_ch
      if p <> '' then row= row||'|'||substr(h,j,1)
      t= t||row||eol
      j= j + 1   
   end 
   say t
return
Output:
Polybius square:

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

Only characters from the 'ATTACKAT1200AM'

A D F G V X | 
- - - - - - |-
            |A
  T   C     |D
A           |F
    1       |G
  2         |V
    0 K M   |X

Conversion by table: 

ATTACKAT1200AM ==> FADDDDFADGXGFADDGFVDXFXFFAXV

L I F E G U A R D 
- - - - - - - - - 
F A D D D D F A D 
G X G F A D D G F 
V D X F X F F A X 
V                 

Sorted by columns:

A D E F G I L R U 
- - - - - - - - - 
F D D D D A F A D 
D F F G A X G G D 
F X F X X D V A F 
            V     

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Encrypted message: FDF  DFX  DFF  DGX  DAX  AXD  FGVV AGA  DDF  
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~