Search code examples
numbersmask

How to mask number to look as it would be random value


Database resources, that can be accessed from webpage that I'm currently working on, have unique id number with auto_increment set. So url would have to look like some.web.page.com/resource/id-number.

It would be kinda easy for user to notice that he can simply increase or decrease number at the end to get anything he pleases and while security isn't big concern in this case, I would really like to prevent that kind of behavior.

I was trying to find some function that would convert the number to random string look-a-like, but I failed (didn't really know what to put in that field on google.com ;) ). I also have my own ideas, but I prefer to use method that is already working well somewhere. The function needs to be symmetrical so I can easily generate string, and get number from that string. Any advice?


Solution

  • Ray Morgan gives an algorithm and an implementation in PHP. The algorithm has a few nice properties, namely:

    • the algorithm is deterministic, i.e., always produces the same obfuscated string for a given numeric ID value.
    • the obfuscation is fully invertible, i.e., if you know (only) the obfuscated value, you can extract the underlying numeric ID
    • doesn't yield any recognizable patterns (such as simple increasing sequences of integers)
    • it can detect, whether an obfuscated ID string has been tampered with

    The author itself explains the basic steps as follows

    • Create a random number ($segment1) based on a hash of $id.
    • Create a second random number ($segment2) based on a hash of $segment1.
    • Alter $segment2 by adding or subtracting the value of $id.
    • Make a third hash ($segment3) from $segment1 and the altered $segment2. This hash makes it possible to detect any alteration of the encoded ID.
    • Concatenate the three segments into a string,
    • and voilà – you have your obfuscated ID.

    For those like me not comfortable with PHP, a working Common Lisp port of the algorithm could look like:

    #-(and) (ql:quickload "ironclad")
    #-(and) (ql:quickload "trivial-utf-8")
    
    (defpackage "HASHID"
      (:use "COMMON-LISP" "IRONCLAD" "TRIVIAL-UTF-8")
      (:shadowing-import-from "COMMON-LISP" "NULL"))
    
    (in-package "HASHID")
    
    (defparameter +secret+ "Secret Password")
    
    (defun sha1-hex-digest (string &optional (secret +secret+))
      (let ((digest (make-digest :sha1)))
        (update-digest digest (string-to-utf-8-bytes string))
        (update-digest digest (string-to-utf-8-bytes secret))
        (let* ((result (produce-digest digest))
               (length (length result))
               (char-length (* length 2))
               (buffer (make-array char-length :element-type 'character))
               (digits "0123456789ABCDEF"))
          (loop
             :with wp := 0
             :for byte :across result
             :do (setf (char buffer (prog1 wp (incf wp))) (char digits (ash byte -4)))
                 (setf (char buffer (prog1 wp (incf wp))) (char digits (logand byte 15)))
             :finally (return buffer)))))
    
    
    (defun obfuscate-id (identifier)
      (let* ((segment-1 (subseq (sha1-hex-digest (format nil "~D" identifier)) 0 16))
             (segment-2 (subseq (sha1-hex-digest (concatenate 'string segment-1)) 0 8))
             (decimal (parse-integer segment-2 :radix 16))
             (buried-id (if (< identifier decimal) (- decimal identifier) (+ decimal identifier)))
             (new-segment-2 (format nil "~8,'0X" buried-id))
             (segment-3 (subseq (sha1-hex-digest (concatenate 'string segment-1 new-segment-2)) 0     8)))
        (concatenate 'string segment-1 new-segment-2 segment-3)))
    
    
    (defun deobfuscate-id (string)
      (let* ((segment-1 (subseq string 0 16))
             (segment-2 (subseq string 16 24))
             (segment-3 (subseq string 24))
             (expected-2 (subseq (sha1-hex-digest segment-1) 0 8))
             (expected-3 (subseq (sha1-hex-digest (concatenate 'string segment-1 segment-2)) 0 8)))
        (and (string-equal segment-3 expected-3)
             (let* ((v1 (parse-integer segment-2 :radix 16))
                    (v2 (parse-integer expected-2 :radix 16)))
               (abs (- v1 v2))))))
    

    Note, that the original implementation generated a base-64 encoded string from the obfuscated ID and used that as the actual value. I did omit this step here, but it should be simple to add, in particular, if your programming language of choice comes with base-64 support.