;; hill.lisp - A very bad implementation of a hill cipher along with some ;; unnessesary, insecure PRNG key stuff. ;; ;; This file is free software under the terms of the GPL3 license. You can find ;; a copy of the license at: https://www.gnu.org/licenses/gpl-3.0.en.html ;; PRNG subroutines (defparameter *system-rng-stream* (open "/dev/random" :element-type '(unsigned-byte 8))) (defun number-bits (num) "Return the number of bits in NUM." (ceiling (log num 2))) (defun get-random (&key (size 8) (rng-stream *system-rng-stream*)) "Read SIZE random bytes from the systems high-quality random source." (let ((read-buf (make-array size))) (read-sequence read-buf rng-stream) (loop with out-val = 0 for part across read-buf do (setq out-val (+ (ash out-val 8) part)) finally (return out-val)))) (defun xorshift*64 (seed) "Generate the next number in the xorshift* sequence starting or continuing from SEED. That is, SEED should either be an initial, non-zero seed or the last number generated in the sequence." (setq seed (logxor seed (ash seed -12)) seed (logxor seed (ash seed 25)) seed (logxor seed (ash seed -27))) ;; #x2545F4914F6CDD1DULL (logand (* seed 2685821657736338717) #xFFFFFFFFFFFFFFFF)) (defun rand-clamp (num &key (bits 64) (min 0 has-min-p) (max (1- (expt 2 bits)) has-max-p)) "Clamp the randomly generated number NUM to between [MIN, MAX]. The random numbers is assumed to be between 0 and 2^BITS - 1." (if (or has-min-p has-max-p) (+ (* (- max min) (/ num (1- (expt 2 bits)))) min) num)) (defclass xorshift*64-stream () ((state :initarg :seed :initform 1 :documentation "The last generated number. If no number has yet been generated, this is the seed.")) (:documentation "A stream of random numbers generated and xorshift*64 algorithm.")) (defun make-xorshift* (&key (seed (get-random)) (bits 64)) (case bits (64 (make-instance 'xorshift*64-stream :seed seed)) (t (error "Invalid prandom number size: ~d" bits)))) (defmethod next-int ((stream xorshift*64-stream) &key (max #xFFFFFFFFFFFFFFFF) (min 0)) "Get the next random integer between MIN and MAX from STREAM." (with-slots (state) stream (setf state (xorshift*64 state)) (floor (rand-clamp state :bits 64 :max max :min min)))) (defparameter *default-rng* (make-xorshift*) "The default RNG source to use when generating matrices.") ;; Matrix subroutines (defun mat-minor (transpose-mat i j) "Find the minor of MAT for I and J." (destructuring-bind (height width) (array-dimensions transpose-mat) (let ((minor (make-array (list (1- height) (1- width))))) (dotimes (row height minor) (dotimes (col width) (unless (or (= row i) (= col j)) (let ((out-row (if (> row i) (1- row) row)) (out-col (if (> col j) (1- col) col))) (setf (aref minor out-row out-col) (aref transpose-mat row col))))))))) (defun cofactor-sgn (i j) "Return the sign of the cofactor at I and J." (expt -1 (+ i j))) (defun cofactor (mat i j) "Find the cofactor for I and J in MAT." (* (cofactor-sgn i j) (det (mat-minor mat i j)))) (defun first-column-cofactors (mat) "Find the cofactors for the first column of MAT." (let* ((height (array-dimension mat 0)) (out-arr (make-array height))) (dotimes (i height out-arr) (setf (aref out-arr i) (cofactor mat i 0))))) (defun det2x2 (mat) "Find the determinate of a 2x2 matrix MAT." (let ((a (aref mat 0 0)) (b (aref mat 0 1)) (c (aref mat 1 0)) (d (aref mat 1 1))) (- (* a d) (* b c)))) (defun det (mat &key first-column-cofactors) "Find the determinant of MAT. If the cofactors for the first column have already been calculated, they can be supplied in FIRST-COLUMN-COFACTORS." (destructuring-bind (height width) (array-dimensions mat) (if (and (= height 2) (= width 2)) (det2x2 mat) (loop for i below height when first-column-cofactors summing (* (aref mat i 0) (aref first-column-cofactors i)) else summing (* (aref mat i 0) (cofactor mat i 0)))))) (defun map-mat (function mat) "Return a new matrix of the same dimensions as MAT, with each element being the result of applying FUNCTION to each index of MAT. FUNCTION is passed two values, (i j), where i and j are the row and column of the element." (destructuring-bind (height width) (array-dimensions mat) (let ((out-mat (make-array (list height width)))) (dotimes (row height out-mat) (dotimes (col width) (setf (aref out-mat row col) (funcall function row col))))))) (defun extended-gcd (n1 n2) "Return the greatest common denominator of N1 and N2. As a second value, return a cons of (x . y) such char x * N1 + y * N2 = GCD." (if (zerop n1) (values n2 (cons 0 1)) (multiple-value-bind (div rem) (floor n2 n1) (destructuring-bind (gcd (x . y)) (multiple-value-list (extended-gcd rem n1)) (values gcd (cons (- y (* div x)) x)))))) (defun modular-multiplicative-inverse (n m) "Find the modular multiplicative inverse of N with respect to M. That is, some integer i such that (N * i) mod M = 1." (destructuring-bind (gcd (x . y)) (multiple-value-list (extended-gcd n m)) (declare (ignorable y)) (unless (= 1 gcd) (error "N and M must be coprime. N: ~d, M: ~d" n m)) (+ x m))) (defun modular-inverse-det (mat modulo &key first-column-cofactors) "Calculate the inverse determinant of MAT under MODULO. If MODULO is nil, this is just 1/det." (let ((det (det mat :first-column-cofactors first-column-cofactors))) (if modulo (modular-multiplicative-inverse det modulo) (/ det)))) (defun invert-2x2-mat (mat &key modulo) "Invert the 2x2 matrix MAT." (let ((a (aref mat 0 0)) (b (aref mat 0 1)) (c (aref mat 1 0)) (d (aref mat 1 1)) (ood (modular-inverse-det mat modulo))) (make-array '(2 2) :initial-contents (list (list (* ood d) (* ood (- b))) (list (* ood (- c)) (* ood a)))))) (defun invert-mat (mat &key modulo) "Invert MAT. This naively assumes that MAT is not singular." (destructuring-bind (height width) (array-dimensions mat) (if (and (= width 2) (= height 2)) (invert-2x2-mat mat :modulo modulo) (let* ((first-column-cofactors (first-column-cofactors mat)) (one-over-det (modular-inverse-det mat modulo :first-column-cofactors first-column-cofactors))) (map-mat (lambda (i j) ;; this calculates 1/det * adjugate[i][j] (* one-over-det (if (and first-column-cofactors (zerop i)) (aref first-column-cofactors j) (cofactor mat j i)))) mat))))) (defun transpose-mat (mat) "Transpose MAT." (map-mat (lambda (i j) (aref mat j i)) mat)) (defun dot-row-col (mat1 mat2 row col) "Take the dot (scalar) product of ROW of MAT1 and COL of MAT2." (let ((sum 0)) (dotimes (n (array-dimension mat2 0) sum) (setq sum (+ sum (* (aref mat1 row n) (aref mat2 n col))))))) (defun *mm (mat1 mat2 &key modulo) "Multiply MAT1 by MAT2. This naively assumes that these matrices can be multiplied without issue. The result will be taken under MODULO, if it is non-nil." (let* ((width (array-dimension mat1 0)) (height (array-dimension mat2 1)) (out-mat (make-array (list height width)))) (dotimes (i height out-mat) (dotimes (j width) (let ((dot (dot-row-col mat1 mat2 i j))) (setf (aref out-mat i j) (if modulo (mod dot modulo) dot))))))) (defun *mv (mat vec &key modulo) "Multiply MAT by VEC. This naively assumes that these can be multiplied without issue. The result will be taken under MODULO, if it is non-nil." (let* ((width (array-dimension mat 1)) (height (array-dimension mat 0)) (out-vec (make-array height))) (dotimes (row height out-vec) (setf (aref out-vec row) (loop for col below width summing (* (aref vec col) (aref mat row col)) into sum finally (return (if modulo (mod sum modulo) sum))))))) (defun generate-matrix (width height &key (rng *default-rng*) max) "Generate a random matrix of size WIDTH x HEIGHT. Use RNG to get the random data for this matrix." (map-mat (lambda (i j) (declare (ignorable i j)) (if max (next-int rng :max max) (next-int rng))) (make-array (list height width)))) (defun generate-invertible-matrix (width height &key (rng *default-rng*) max) "Generate a random invertible matrix of size WIDTH x HEIGHT. Use RNG to get the random data for this matrix. This generates a strictly diagonally dominant matrix." (let ((rand-mat (generate-matrix width height :rng rng :max max))) (dotimes (row height rand-mat) (let ((sum 0)) (dotimes (col width) (setq sum (+ sum (abs (aref rand-mat row col))))) (setf (aref rand-mat row row) sum))))) ;; Base64 subroutines (defparameter +base64-alphabet+ "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" "Lookup table for base64 encode.") (defparameter +inverse-base64-alphabet+ (loop with alph = (make-array 123 :initial-element 0) for i below (length +base64-alphabet+) for char = (aref +base64-alphabet+ i) do (setf (aref alph (char-code char)) i) finally (return alph)) "Inverse lookup table for base64 decode.") (defun base64-encode-block (b1 &optional b2 b3) "Base64 encode B1, B2, and B3, each of which should be a single byte. The result of this function is a list of the 4 encoded characters." ;; allow B2 and B3 to be nil, so we can't just check if they where passed, we ;; need to know if they where nil when the function was called (let ((has-b2-p b2) (has-b3-p b3)) (unless b2 (setq b2 0)) (unless b3 (setq b3 0)) (let ((e1 (ash (logand b1 #b11111100) -2)) (e2 (logior (ash (logand b1 #b11) 4) (ash (logand b2 #b11110000) -4))) (e3 (logior (ash (logand b2 #b1111) 2) (ash (logand b3 #b11000000) -6))) (e4 (logand b3 #b111111))) (cond ((and has-b2-p has-b3-p) (list (aref +base64-alphabet+ e1) (aref +base64-alphabet+ e2) (aref +base64-alphabet+ e3) (aref +base64-alphabet+ e4))) (has-b2-p (list (aref +base64-alphabet+ e1) (aref +base64-alphabet+ e2) (aref +base64-alphabet+ e3) #\=)) (t (list (aref +base64-alphabet+ e1) (aref +base64-alphabet+ e2) #\= #\=)))))) (defun base64-encode (data) "Encode DATA, a vector of numbers, as base64." (loop with output = () for i upfrom 0 by 3 below (- (length data) 2) do (setf output (nconc (nreverse (base64-encode-block (aref data i) (aref data (+ i 1)) (aref data (+ i 2)))) output)) finally (return (coerce (if (< i (length data)) (let ((b1 (aref data i)) (b2 (when (< (1+ i) (length data)) (aref data (1+ i))))) (nreverse (nconc (nreverse (base64-encode-block b1 b2)) output))) (nreverse output)) 'string)))) (defun base64-decode-block (e1 e2 e3 e4) "Base64 decode E1, E2, E3, and E4, each of which is a single character. The result of this function is a list of 3 bytes." (let ((i1 (aref +inverse-base64-alphabet+ (char-code e1))) (i2 (aref +inverse-base64-alphabet+ (char-code e2))) (i3 (aref +inverse-base64-alphabet+ (char-code e3))) (i4 (aref +inverse-base64-alphabet+ (char-code e4)))) (list (logior (ash i1 2) (ash (logand i2 #b110000) -4)) (unless (eq e3 #\=) (logior (ash (logand i2 #b1111) 4) (ash (logand i3 #b111100) -2))) (unless (eq e4 #\=) (logior (ash (logand i3 #b11) 6) i4))))) (defun base64-decode (data) "Decode DATA, a string, as base64." (coerce (loop for i upfrom 0 by 4 below (length data) append (delete-if 'null (base64-decode-block (aref data i) (aref data (+ i 1)) (aref data (+ i 2)) (aref data (+ i 3))))) 'vector)) ;; Hill subroutines (defun nth-hill-block (plain n &key (size 4)) "Get the Nth hill block in PLAIN. If there are not enough characters in PLAIN, pad the block with zeros. Each block is SIZE characters." (let* ((start (* size n)) (end (+ start size))) (when (< (length plain) end) (setq end (+ start (- (length plain) start)))) (cond ((> start (length plain)) (make-array size :initial-element 0)) ((>= end (length plain)) (loop with out-vec = (make-array size :initial-element 0 :fill-pointer 0) for elem across (subseq plain start (length plain)) do (vector-push elem out-vec) finally (setf (fill-pointer out-vec) size) (return out-vec))) (t (subseq plain start end))))) (defun hill-block-count (plain &key (size 4)) "Return the number of hill blocks in PLAIN." (ceiling (length plain) size)) (defmacro do-hill-blocks ((var plain &optional retval) (&key (size 4)) &body body) "Evaluate BODY once for each hill cipher block in PLAIN, with VAR bound to that block." (let ((i-var (gensym)) (plain-var (gensym))) `(let ((,plain-var ,plain)) (dotimes (,i-var (hill-block-count ,plain-var :size ,size) ,retval) (let ((,var (nth-hill-block ,plain-var ,i-var :size ,size))) ,@body))))) (defun make-hill-matrix (key &key (size 4)) "Get the hill cipher matrix for KEY and block size SIZE." (let ((rng (make-xorshift* :seed key))) (generate-invertible-matrix size size :rng rng :max 128))) (defun hill-process (data mat size dict-size) "Perform hill encryption or decryption on DATA. Each block is SIZE bytes. MAT is taken to be the key matrix (or its inverse). DICT-SIZE is the modulus." (let ((processed (make-array 0))) (do-hill-blocks (bl data (map 'vector (lambda (elem) (mod elem dict-size)) processed)) (:size size) (setq processed (concatenate 'vector processed (*mv mat bl)))))) (defun hill-encrypt (plain key &key (size 4) (dict-size 251)) "Encrypt PLAIN using a hill cipher, using KEY as the secret and SIZE as the block size. DICT-SIZE is the largest character code in PLAIN." (hill-process (map 'vector 'char-code plain) (make-hill-matrix key :size size) size dict-size)) (defun make-inverse-hill-matrix (key &key (size 4) (dict-size 251)) "Create an inverse hill matrix for KEY. SIZE and DICT-SIZE are as they are to `hill-decrypt'." (invert-mat (make-hill-matrix key :size size) :modulo dict-size)) (defun hill-remove-padding (plain-vec) "Remove trailing null bytes from PLAIN-VEC." (loop for i downfrom (1- (length plain-vec)) while (and (>= i 0) (eq (aref plain-vec i) #\Nul)) finally (return (subseq plain-vec 0 (1+ i))))) (defun hill-decrypt (cipher key &key (size 4) (dict-size 251)) "Perform the inverse operation of `hill-encrypt', restoring the plaintext of CIPHER that was encrypted using KEY with a block size of SIZE. DICT-SIZE must be the same value originally used to encrypt the text." (coerce (hill-remove-padding (map 'vector 'code-char (hill-process cipher (make-inverse-hill-matrix key :size size :dict-size dict-size) size dict-size))) 'string)) ;; Local Variables: ;; jinx-local-words: "adjugate cofactor cofactors det plaintext prandom unnessesary xorshift" ;; End: