Say you have the value $n$ which is the size of the alphabet (i.e. the range of characters that make up your plaintext and ciphertext). And say $m = x \times n$ is the largest value you have that is not higher than $y$ where $y$ is the largest size produced by your random number generator.
For each character:
- Take values from the random number generator until you have a value smaller than $m$, call this value $r_i$;
- Calculate $s_i$ which is simply $r_i \bmod n$;
- Calculate $p_i$ which is the location of the current character in your alphabet;
- Calculate $c_i = (p_i + s_i) \bmod n$
- Take the character at location $c_i$ of the alphabet and display.
To reverse:
- Take values from the random number generator until you have a value smaller than $m$, call this value $r_i$;
- Calculate $s_i$ which is simply $r_i \bmod n$;
- Calculate $c_i$ which is the location of the current character in your alphabet (the first character in the alphabet is on position 0);
- Calculate $p_i = (c_i - s_i) \bmod n$
- Take the character at location $p_i$ of the alphabet and display.
In the above calculations I presume that the random number generator produces numbers from $0$ up to $y$ (so without $y$). The characters are in an alphabet where they have an index from $0$ up to $n$ (so without $n$).
If $y = n$ (the maximum value of the random number generator is the same as the size of the alphabet) then the scheme is of course simpler. You can just take the next value of the random number generator and call it $s_i$, skipping the first two steps.
Example implementation in Java (sorry, don't know Python that well):
import java.nio.charset.StandardCharsets;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
public class StreamCryptUsingAlphabet {
private String alphabet;
private int n;
private int y;
private int m;
private SecureRandom rng;
public StreamCryptUsingAlphabet(String alphabet, int y) {
this.alphabet = alphabet;
this.n = alphabet.length();
if (y < n) {
throw new IllegalArgumentException("y is too small");
}
this.y = y;
// % is the remainder operator, same as modulus for positive numbers
this.m = y - y % n;
}
public void init(byte[] seed) {
try {
rng = SecureRandom.getInstance("SHA1PRNG");
} catch (NoSuchAlgorithmException e) {
throw new IllegalStateException("RNG not available");
}
rng.setSeed(seed);
}
private int nextS() {
int r;
do {
r = rng.nextInt(y);
} while (r >= m);
return r % n;
}
public String encrypt(String plaintext) {
String ciphertext = "";
for (int i = 0; i < plaintext.length(); i++) {
int p = indexInAlphabet(plaintext.charAt(i));
int s = nextS();
int c = (p + s) % n;
ciphertext += characterInAlphabet(c);
}
return ciphertext;
}
public String decrypt(String ciphertext) {
String plaintext = "";
for (int i = 0; i < ciphertext.length(); i++) {
int c = indexInAlphabet(ciphertext.charAt(i));
int s = nextS();
// Java's remainder requires me to add n first
int p = (c - s + n) % n;
plaintext += characterInAlphabet(p);
}
return plaintext;
}
private int indexInAlphabet(char c) {
String s = String.valueOf(c);
return alphabet.indexOf(s);
}
private char characterInAlphabet(int i) {
return alphabet.charAt(i);
}
public static void main(String[] args) {
byte[] key = "Khan".getBytes(StandardCharsets.US_ASCII);
StreamCryptUsingAlphabet sc = new StreamCryptUsingAlphabet(
"abcdefghijklmnopqrstuvwxyz", 80);
sc.init(key);
String ct = sc.encrypt("owlstead");
sc.init("Khan".getBytes(StandardCharsets.US_ASCII));
String pt = sc.decrypt(ct);
System.out.println(pt);
}
}
The next part explains what is usually done if a key stream which is a stream of pseudo random bits...
Normally, if you've generated a random key stream, then we use XOR to encrypt the plain text. For this the plaintext needs to be binary as well. So if the plaintext is actually text, we need to perform character encoding on it. Commonly used encodings are US ASCII and UTF-8. This will however result in a binary ciphertext, so if you want to represent it as a string, you can use base 64 encoding.
To decrypt you first base 64 decode the data you've received to retrieve the ciphertext. Then you decrypt, and afterwards you use character decoding to retrieve the text from the binary plaintext.
So the above is what is usually done within computers.