Skip to main content

How to Implement a Simple Substitution Cipher in Batch Script

A substitution cipher is a method of encrypting text by replacing each character with another character according to a fixed system. While it is not secure enough for sensitive data by modern standards, it is an excellent exercise for mastering string slicing, character replacement, and loop-based text processing in Batch.

In this guide, we will demonstrate how to build a simple ROT13-style encoder/decoder and a more flexible map-based substitution script.

Method 1: The Character-by-Character Mapping

This method iterates through the input string one character at a time, looks up each character's position in a source alphabet, and substitutes the character at the same position in a cipher alphabet. By defining an "Alpha" string and its corresponding "Cipher" map, we build the encrypted output in a separate variable, avoiding collision issues entirely.

Implementation Script

@echo off
setlocal enabledelayedexpansion

:: Define the standard alphabet and the shifted cipher
:: In this example, A becomes N, B becomes O, etc. (ROT13)
set "Alpha=ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
set "Cipher=NOPQRSTUVWXYZABCDEFGHIJKLMnopqrstuvwxyzabcdefghijklm"

set "InputText="
set /p "InputText=Enter text to process: "

if not defined InputText (
echo [ERROR] No input provided.
pause
exit /b 1
)

set "OutputText="

:: Loop through every character in the input string
set "Index=0"
:ProcessLoop
set "Char=!InputText:~%Index%,1!"
if not defined Char goto :DisplayResult

:: Find the character's position in the Alpha string
set "Found=0"
for /L %%i in (0, 1, 51) do (
if !Found! equ 0 (
if "!Char!"=="!Alpha:~%%i,1!" (
set "MapChar=!Cipher:~%%i,1!"
set "OutputText=!OutputText!!MapChar!"
set "Found=1"
)
)
)

:: If character was not in alphabet (e.g., space, digit, punctuation), keep it unchanged
if !Found! equ 0 set "OutputText=!OutputText!!Char!"

set /a "Index+=1"
goto :ProcessLoop

:DisplayResult
echo.
echo Original: !InputText!
echo Result: !OutputText!
echo.
pause
exit /b 0
tip

ROT13 is a symmetric cipher: applying it twice returns the original text. This means running the encrypted output back through the same script will decrypt it automatically. This property makes ROT13 convenient for simple obfuscation where the same script serves as both encoder and decoder.

How It Works

  1. The script extracts one character at a time from InputText using !InputText:~%Index%,1!.
  2. It compares that character against every position in the Alpha string using a for /L loop.
  3. When a match is found, it retrieves the character at the same position in the Cipher string and appends it to OutputText.
  4. Characters not found in Alpha (spaces, digits, punctuation) are appended unchanged.
  5. The loop advances the index and repeats until no more characters remain.
warning

This method processes characters by iterating through the entire alphabet for every input character, making it O(n × m) where n is the input length and m is the alphabet size. For short messages (under a few hundred characters), this is perfectly acceptable. For very long texts, consider the PowerShell approach in Method 3.

Method 2: Bulk String Replacement with Collision Avoidance

The character-by-character loop in Method 1 is precise but verbose. Batch's native string replacement (!var:OLD=NEW!) can transform an entire string in one operation, but it introduces a critical problem: if you replace A with B and then B with C, every original A becomes C instead of B. This is called a collision.

The solution is a two-pass approach using temporary placeholder strings that do not appear in the input.

Implementation Script

@echo off
setlocal enabledelayedexpansion

set "Message="
set /p "Message=Enter message: "

if not defined Message (
echo [ERROR] No input provided.
pause
exit /b 1
)

set "Result=!Message!"

:: Example cipher: Swap A<->Z, B<->Y, C<->X (and lowercase equivalents)
:: Pass 1: Replace source characters with unique placeholders
set "Result=!Result:A=__01__!"
set "Result=!Result:Z=__02__!"
set "Result=!Result:B=__03__!"
set "Result=!Result:Y=__04__!"
set "Result=!Result:C=__05__!"
set "Result=!Result:X=__06__!"
set "Result=!Result:a=__07__!"
set "Result=!Result:z=__08__!"
set "Result=!Result:b=__09__!"
set "Result=!Result:y=__10__!"
set "Result=!Result:c=__11__!"
set "Result=!Result:x=__12__!"

:: Pass 2: Replace placeholders with target characters
set "Result=!Result:__01__=Z!"
set "Result=!Result:__02__=A!"
set "Result=!Result:__03__=Y!"
set "Result=!Result:__04__=B!"
set "Result=!Result:__05__=X!"
set "Result=!Result:__06__=C!"
set "Result=!Result:__07__=z!"
set "Result=!Result:__08__=a!"
set "Result=!Result:__09__=y!"
set "Result=!Result:__10__=b!"
set "Result=!Result:__11__=x!"
set "Result=!Result:__12__=c!"

echo.
echo Original: !Message!
echo Encoded: !Result!
echo.
pause

Why Placeholders Are Necessary

Without placeholders, a direct replacement chain produces incorrect results:

:: WRONG - Collision: A→Z, then Z→A reverses the first replacement
set "Result=!Result:A=Z!"
set "Result=!Result:Z=A!"
:: Every original A becomes Z, then immediately becomes A again

The two-pass approach ensures that no replacement can interfere with another, because the intermediate placeholder strings (__01__, __02__, etc.) are guaranteed not to appear in normal input text.

info

Batch string replacement (!var:OLD=NEW!) is case-insensitive by default. This means !var:A=Z! replaces both A and a. In Method 2, we handle each case explicitly with separate placeholders to maintain case-sensitive substitution. If case-insensitive replacement is acceptable for your use case, you can halve the number of replacement operations.

Method 3: PowerShell Base64 Encoding

If your goal is simple obfuscation rather than a letter-swap exercise, Base64 is the industry standard. While technically an encoding rather than a cipher, it hides plain text from casual observation and handles special characters much more reliably than native Batch set commands.

Implementation Script

@echo off
setlocal enabledelayedexpansion

set "Original=Secret Message 123!"

:: Encode to Base64
set "Encoded="
for /f "delims=" %%I in ('powershell -NoProfile -Command ^
"[Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes('!Original!'))"') do (
set "Encoded=%%I"
)

if not defined Encoded (
echo [ERROR] Encoding failed.
pause
exit /b 1
)

:: Decode from Base64
set "Decoded="
for /f "delims=" %%I in ('powershell -NoProfile -Command ^
"[System.Text.Encoding]::UTF8.GetString([Convert]::FromBase64String('!Encoded!'))"') do (
set "Decoded=%%I"
)

echo Original: !Original!
echo Encoded: !Encoded!
echo Decoded: !Decoded!

:: Verify round-trip integrity
if "!Decoded!"=="!Original!" (
echo.
echo [OK] Round-trip verification passed.
) else (
echo.
echo [WARNING] Decoded text does not match original.
echo The input may contain characters that break Batch variable expansion.
)

pause
warning

When the input text contains single quotes ('), double quotes ("), percent signs (%), ampersands (&), or other Batch special characters, passing it through !Original! into the PowerShell command string can break the command or produce incorrect results. For inputs with arbitrary characters, write the text to a temporary file and have PowerShell read from the file instead of the command line.

Common Mistakes

The Wrong Way: Replacing Characters In-Place Without Collision Protection

:: WRONG - Replacements interfere with each other
set "Text=ABCABC"
set "Text=!Text:A=B!"
set "Text=!Text:B=C!"
set "Text=!Text:C=D!"
echo !Text!
:: Expected: BCDBC Actual: DDDDDD
danger

Each replacement operates on the result of the previous one. After A→B, the string contains only Bs and Cs. Then B→C converts everything to Cs. Finally C→D converts everything to Ds. Use Method 1 (character-by-character with a separate output variable) or Method 2 (two-pass placeholder approach) to avoid this.

The Wrong Way: Using %Char% Instead of !Char! Inside Loops

:: WRONG - %Char% is expanded at parse time, not at runtime
set "Char=!InputText:~%Index%,1!"
if "%Char%"=="" goto :DisplayResult
warning

Inside a goto-based loop, variables set with set are not updated when referenced with %var% syntax because %var% is expanded when the line is first parsed, not when it executes. Use !var! (delayed expansion) for all variables that change during loop execution. The corrected check is if not defined Char goto :DisplayResult.

Best Practices

  1. Handle special characters defensively: Native Batch is fragile when processing characters like ^, %, &, <, >, and !. If your cipher script encounters these in the input, set commands may break or produce incorrect results. Use delayed expansion and test with special-character inputs.
  2. Use explicit case handling: Batch string replacement is case-insensitive by default. In Method 1, we include both uppercase and lowercase characters in the Alpha and Cipher strings to ensure A and a map to different cipher characters. In Method 2, separate placeholders preserve case distinction.
  3. Avoid in-place collision: Never run multiple set "var=!var:A=B!" replacements on the same variable for a substitution cipher. Use either character-by-character output building (Method 1) or two-pass placeholder replacement (Method 2).
  4. Use Base64 for practical obfuscation: For real-world obfuscation of configuration values or non-critical strings, Base64 (Method 3) is more reliable and handles the full character set without Batch parsing issues.
  5. Never use this for security: Batch-based substitution ciphers provide zero cryptographic security. They are strictly for educational purposes, simple obfuscation, or adding personality to automation scripts. For actual encryption, use PowerShell with [System.Security.Cryptography] classes or dedicated tools.

Conclusion

Implementing a substitution cipher in Batch forces a deep dive into string manipulation and loop control.

  • The character-by-character approach (Method 1) provides the most correct and collision-free implementation by building the output in a separate variable.
  • The bulk replacement approach (Method 2) is faster but requires careful two-pass placeholder logic to prevent replacement collisions.
  • For practical obfuscation needs, PowerShell Base64 encoding (Method 3) handles the full character set reliably.

Regardless of the method chosen, the exercise builds essential skills in Batch string slicing, delayed expansion, and iterative processing.