26 September 2022

Rot13 to the Rescue!

A convenient way to obscure something you'd rather not see all the time.

A few times in my career I've worked on small modules that generated random/unique alphanumeric codes. Most recently, the module in question was written in Clojure, something like the code below.

(def ambiguous-characters #{\O \0 \1 \I})
(def alphabet (map char (range (int \A) (inc (int \Z)))))
(def code-chars
  (->> (concat alphabet (range (int \0) (inc (int \9))))
       (map char)
       (remove ambiguous-characters)))

(defn unique-readable-code [length]
  (->> (repeatedly #(rand-nth code-chars))
       (take length)
       (apply str)))

That code creates things like this:

user=> (unique-readable-code 10)
"EYN2N5EE7K"

There was a mechanism already in place to prevent objectionable words from being generated, something like the following:

(require 'clojure.string)

(def bad-words (map rot13 ["SHUCKS" "DARN" "HECK"]))

(defn has-bad-word? [code]
  (some #(clojure.string/includes? code %) bad-words))

(defn infinite-list-of-codes [len]
  (->> (repeatedly #(unique-readable-code len))
       (remove has-bad-word?)))

So you could easily generate lots of codes:

user=> (take 9 (infinite-list-of-codes 10))
("WDJG7ZSE26" "4PE53YWLUD" "M9LWZMPHXA"
 "N3ZJT2Q3EC" "7SZEQGG8TS" "MALAWR6ZA2" 
 "QLN4RA2K8A" "Q939Q7DXJ8" "VCP4NVQTKY")

Well, I got tired of looking at the actual list of bad-words, which was much longer and much more objectionable than the list shown above. So, I decided to encode it using rot-13.

(def rot13-translations
  (->> (cycle alphabet)
       (drop 13)
       (take 26)
       (zipmap alphabet)))

(defn rot13 [text]
  (->> (clojure.string/upper-case text)
       (replace rot13-translations)
       (apply str)))

Rot-13 has a cyclical quality, meaning that calling it once encodes, calling it again with the output of the first call decodes.

user=> (rot13 "A")
"N"
user=> (rot13 (rot13 "A"))
"A"

Put it all together and you have a nice way to remove objectionable words without needing to see them (except to add new ones).

(require 'clojure.string)

(def alphabet (map char (range (int \A) (inc (int \Z)))))

(def rot13-translations
  (->> (cycle alphabet)
       (drop 13)
       (take 26)
       (zipmap alphabet)))

(defn rot13 [text]
  (->> (clojure.string/upper-case text)
       (replace rot13-translations)
       (apply str)))

; Paste this definition into a REPL to view current listing.
; Use the rot13 fn above to encode additional words.
(def bad-words (map rot13 ["FUHPXF" "QNEA" "URPX"]))

(defn has-bad-word? [code]
  (some #(clojure.string/includes? code %) bad-words))

(def code-chars
  (->> (concat alphabet (range (int \0) (inc (int \9))))
       (map char)
       (remove #{\O \0 \1 \I})))

(defn unique-readable-code [length]
  (->> (repeatedly #(rand-nth code-chars))
       (take length)
       (apply str)))

(defn infinite-list-of-codes [len]
  (->> (repeatedly #(unique-readable-code len))
       (remove has-bad-word?)))

;;;;;;;;;;;;;;;;;;;;;;;

(it "rot13"
  (let [input "INPUT"]
    (should-not= input (->> input codes/rot13))
    (should= input (->> input codes/rot13 codes/rot13))))

(it "identifies bad words"
  (let [code (codes/rot13 "URYY")]
    (should (codes/has-bad-word? code))))