This may be more than what you are looking for, but I thought it would be fun to construct as an answer. Here is a simple format-preserving encryption which takes any 16-bit number (i.e. from 0 to 65535) and encrypts it to another 16-bit number and back again, based on a 128-bit symmetric key. You can build something like this.
It's deterministic, in that any input always encrypts to the same output with the same key, but for any number n, there is no way to predict the output for n + 1.
# Written in Ruby -- implement in PHP left as an exercise for the reader
require 'openssl'
def encrypt_block(b, k)
cipher = OpenSSL::Cipher::Cipher.new 'AES-128-ECB'
cipher.encrypt
cipher.key = k
cipher.update(b) + cipher.final
end
def round_key(i, k)
encrypt_block(i.to_s, k)
end
def prf(c, k)
encrypt_block(c.chr, k)[0].ord
end
def encrypt(m, key)
left = (m >> 8) & 0xff
right = m & 0xff
(1..7).each do |i|
copy = right
right = left ^ prf(right, round_key(i, key))
left = copy
end
(left << 8) + right
end
def decrypt(m, key)
left = (m >> 8) & 0xff
right = m & 0xff
(1..7).each do |i|
copy = left
left = right ^ prf(left, round_key(8 - i, key))
right = copy
end
(left << 8) + right
end
key = "0123456789abcdef"
# This shows no fails and no collisions
x = Hash.new
(0..65535).each do |n|
c = encrypt(n, key)
p = decrypt(c, key)
puts "FAIL" if n != p
puts "COLLISION" if x.has_key? c
x[c] = n
end
# Here are some samples
(0..10).each do |n|
c = encrypt(n, key)
p = decrypt(c, key)
puts "#{n} --> #{c}"
end
(0..10).each do
n = rand(65536)
c = encrypt(n, key)
p = decrypt(c, key)
puts "#{n} --> #{c}"
end
Some examples:
0 --> 39031
1 --> 38273
2 --> 54182
3 --> 59129
4 --> 18743
5 --> 7628
6 --> 8978
7 --> 15474
8 --> 49783
9 --> 24614
10 --> 58570
1343 --> 19234
19812 --> 18968
6711 --> 31505
42243 --> 29837
62617 --> 52334
27174 --> 56551
3624 --> 31768
38685 --> 40918
27826 --> 42109
62589 --> 25562
20377 --> 2670