1 06-OneTimePad

Previous: 05-SubstitutionFrequency.html

Meet Alice and Bob (and sometimes evil Eve)!
http://cryptocouple.com/ (show in class)

1.1 Screencasts

1.2 Reading

http://inventwithpython.com/cracking/ Chapter 21

1.3 One Time Pad (OTP)

Our first modern encryption algorithm.
It’s also primitive.
The only timeless encryption algorithm!

One Time Pad (OTP) is not the same thing as a One Time Password (OTP):
is not what we’re talking about today.

What defines a “one time pad”?
The key is at least as long as the message or data that must be encrypted.
The key is truly random.
It is not generated by a deterministic pseudo-random software function.
Key and plain-text are calculated mod the encoding base,
for example:
modulo 10 (digits),
modulo 26 (letters), or
modulo 2 (binary)
mod also works as you would expect in binary (or any base).

Example mod 26:

1.3.1 Pre-shared key

Communication partners must securely pre-share their secret keys!

1.3.2 Perfect secrecy

Information-theoretic security of the correctly implemented OTP is truly perfect,
since the message contains absolutely no “information”.
Given a ciphertext from a correctly implemented OTP,
all plaintext messages are equally likely.

The following are equally likely: Encryption 1:

06-OneTimePad/ex1.jpeg Encryption 2:



1.3.3 Key re-use

Re-using a OTP key is the Vigenere cipher!


1.3.4 Random number generators (RNG)

Cryptographically secure pseudo-random number generator (CSPRNG)
or cryptographic pseudo-random number generator (CPRNG)
is a pseudo-random number generator (PRNG),
with properties that make it suitable for use in cryptography.

Pseudorandom number generators (PRNG)
also known as a deterministic random bit generator (DRBG),
is an algorithm for generating a sequence of numbers,
whose properties approximate the properties of sequences of random numbers.
The PRNG-generated sequence is not truly random,
because it is completely determined by an initial value,
called the PRNG’s seed (which may include truly random values).

There are two primary methods used to generate random numbers.

  1. Physical
    The first method measures some physical phenomenon that is expected to be random,
    and then compensates for possible biases in the measurement process.
    Example sources include measuring atmospheric noise,
    thermal noise, and other external electromagnetic or quantum phenomena.
    For example, cosmic background radiation or radioactive decay,
    as measured over short timescales represent sources of natural entropy.
    In Linux distributions, the pseudo device file /dev/random will block,
    until sufficient entropy is harvested from the environment for retrieval.

  2. Computational
    The second method uses deterministic computational algorithms,
    that can produce long sequences of apparently random results,
    which are completely determined by a short initial seed value,
    known as a seed value, key, or nonce.
    As a result, the entire seemingly random sequence can be reproduced,
    if the seed value is known.
    This type is often called a pseudorandom number generator,
    and typically does not continually rely on sources of naturally occurring entropy,
    though it may be periodically seeded by natural sources to compensate or start.

Cahoot-06.2 In Python3

The module random (random.random, etc.) and numpy.random are not “truly” random:

Use the module called secrets instead:
The secrets module is used for generating cryptographically strong random numbers.
It is suitable for managing data such as:
passwords, account authentication, security tokens, and related secrets.
Secrets should be used in preference to the random module,
which is designed for modeling and simulation,
not for security or cryptography.

To generate a 55-long OTP key, for a 55-long message:

# -*- coding: utf-8 -*-

import secrets

otp: str = ""
for _ in range(55):
    otp += secrets.choice("ABCDEFGHIJKLMNOPQRSTUVWXYZ .")


# or more concisely
otp = "".join([secrets.choice("ABCDEFGHIJKLMNOPQRSTUVWXYZ .") for _ in range(55)])

or from /dev/random for a big integer:

# -*- coding: utf-8 -*-

import os


# or

with open("/dev/random", "rb") as f:
    print(int.from_bytes(f.read(10), "big"))

You could post-process this into random characters.
See this for more detail on (/dev/urandom vs /dev/random|/dev/random.html.html):
https://www.2uo.de/myths-about-urandom/ In Bash

Section # Generate random strings

Discussion Question:
Can you think of any practical issues with actually using the OTP?

1.3.5 Key distribution for the OTP

Requires physical delivery for certainty of perfect security.
Is this practical?
Do other algorithms have a similar constraint?

End-points are vulnerable.
As they are generally, in most security systems.
What else is like this?

Each key is used only once.
Is this practical?

There should only be n copies of the key(s) for n parties,
one for each sender and one for each receiver.

The key material must be securely destroyed after use,
to ensure the key material is never reused,
and to protect the messages sent.

Because the key material must be transported from one endpoint to another,
and persists until the message is sent or received,
it can be more vulnerable to forensic recovery,
compared to the transient plaintext it protects.
How, when?

1.3.6 Code for simple OTP

A simple hack/conversion of some code from our previous algorithms:

# -*- coding: utf-8 -*-
How could this code be improved?
@author: taylor@mst.edu

import secrets


def keygen(keylen: int) -> str:
    return "".join([secrets.choice(LETTERS) for _ in range(keylen)])

def main() -> None:
    myMessage = (
        "The generation of random numbers is too important to be left to chance."
    myMode = "encrypt"  # Set to either 'encrypt' or 'decrypt'.
    if myMode == "encrypt":
        myKey = keygen(len(myMessage))
        print("Your ONE-TIME use key is:\n", myKey, "\n")
        translated = translateMessage(myKey, myMessage, "encrypt")
    elif myMode == "decrypt":
        myKey = input("What was the ONE-TIME use key you securily pre-shared?")
        translated = translateMessage(myKey, myMessage, "decrypt")
    print("Your " + myMode + "ed message was:", translated)

def translateMessage(key: str, message: str, mode: str) -> str:
    translated = []  # Stores the encrypted/decrypted message string.
    keyIndex = 0
    key = key.upper()
    for symbol in message:  # Loop through each symbol in message.
        num = LETTERS.find(symbol.upper())
        if num != -1:  # -1 means symbol.upper() was not found in LETTERS.
            if mode == "encrypt":
                num += LETTERS.find(key[keyIndex])  # Add if encrypting.
            elif mode == "decrypt":
                num -= LETTERS.find(key[keyIndex])  # Subtract if decrypting.
            num %= len(LETTERS)  # Handle any wraparound.
            # Add the encrypted/decrypted symbol to the end of translated:
            if symbol.isupper():
            elif symbol.islower():
            keyIndex += 1  # Move to the next letter in the key.
            # Append the symbol without encrypting/decrypting.
    return "".join(translated)

if _name_ == "_main_":

Taken from which cipher in the book?
Could this be simplified?

This is a base-26 (or symbol-set size base) otp.
One could compute the OTP in any base.
Does the number of symbols impact strength?
Would it be weaker if there were only two symbols?


1.4 XOR

General symbol is \(\oplus\).
Subset of the bitwise operators in Python:

In interpreting the below code,
remember that the ints 1 and 0 are the same as the binary numbers 0 and 1.
The ^ operator operates on the binary representation of int,
or directly on a binary string number,
e.g., 0b11111

# -*- coding: utf-8 -*-

import operator

print(1 ^ 0)
print(0 ^ 1)
print(1 ^ 1)
print(0 ^ 0)

# or where the operator lives
print(operator.xor(1, 0))
print(operator.xor(0, 1))
print(operator.xor(1, 1))
print(operator.xor(0, 0))

# Example on binary strings:
print(31 ^ 30)
print(bin(0b1111 ^ 0b1110))

1.5 Binary OTP

A clean, simple, and general OTP implementation:

  1. Plaintext
  2. Encrypted message / Ciphertext
  1. Length of message
  2. Key

1.5.1 Plaintext space

P = {0, 1}n

1.5.2 Ciphertext space

C = {0, 1}n

1.5.3 Key space

P = {0, 1}n

1.5.4 Encryption

E(p, k) = p \(\oplus\) k

1.5.5 Decryption

D(c, k) = c \(\oplus\) k

Does it even matter what the key-space, plaintext, or ciphertext space is?
What about in the lower limit of message length, 0 or 1 bit?

1.5.6 Example 1

Encrypt by xor’ing input with key:
How to decrypt?
Just repeat to go backwards!

1.5.7 Example 2

n = 5 the length of the data
p = 01011 which is 11
k = 10111 which is 23

c = p \(\oplus\) k
11100 = 01011 \(\oplus\) 10111

Also written as:
c     = p     ^ k
11100 = 01011 ^ 10111

p = c \(\oplus\) k
01011 = 11100 \(\oplus\) 10111

Also written as:
p     = c     ^ k
01011 = 11100 ^ 10111


# -*- coding: utf-8 -*-

n = 5
p = 0b01011
k = 0b10111

print("plaintext is:", p, bin(p))
print("key is:", k, bin(k))

c = p ^ k
print("ciphertext is:", c, bin(c))

p = c ^ k
print("recovered plaintext is:", p, bin(p))

Original message is unambiguously recoverable with XOR,
but not other bit-wise operators, why?

1.5.8 Working with binary data in python

https://www.devdungeon.com/content/working-binary-data-python Check out code:

06-OneTimePad/numeric_bases_encoding.py (review in class)

These two are for you to ponder when doing this in python:

1.5.9 Unicode strings and string encoding in Python


1.6 Binary OTP in python: past assignment

The permissions on this project are institutional.
You just have to be logged into git-classes to see it.

1.7 Is the OTP practical

For personal text-based communications?
Has key-storage changed since this algorithm was used more widely?
For watching videos?
For video-chatting?

1.8 Hardware and other OTP implementations

There are many variants of the OTP, hardware, quantum, light, etc.,
but they’re all just the OTP.
The next time you hear about some new exciting perfect cryptography,
you’ll know it’s just the same old OTP!

Next: 07-CryptoMath.html