Choosing a secure four-digit PIN

Introduction

This article helps you choose a “difficult to guess” four-digit PIN from the 10,000 possibilities between 0000 and 9999. (This is satire – if everyone followed this guide, guessing their PINs would be easier.)

The rules

  1. All intervals between any two digits must be distinct. 0 is considered both the smallest and the largest digit here. For example, no permutation of 0136 or 1380 is allowed. This alone reduces the number of PINs to 1008.
  2. The digits must not be in increasing or decreasing order. Again, 0 is counted as both the smallest and the largest digit. For example, 0137 and 1370 are not allowed.
  3. The PIN must not be a prime power (pn where p is prime and n ≥ 2).
  4. The PIN must not occur in 1000 or more sequences in the OEIS. See the programs below for the list.
  5. The PIN must not be the birth year of anyone alive (currently 1908 to 2024).
  6. The PIN must not be a date in DDMM or MMDD format.
  7. The PIN must not be the name of a popular integrated circuit; see the programs below for the list.
  8. The PIN must not form an English word if the digits are converted into letters like this: 0→O, 1→I/L, 2→Z, 3→E, 4→A, 5→S, 6→G, 7→T, 8→B.

The results

These 753 secure PINs satisfy the rules above:

0581 0592 0694 0738 0782 0783 0791 0793 0837 0935 0946 1037 1046 1058 1064 1073 1079 1094 1096 1275 1284 1286 1294 1376 1384 1387 1394 1398 1428 1429 1438 1439 1482 1483 1492 1493 1498 1527 1572 1586 1587 1628 1629 1637 1640 1658 1682 1685 1692 1697 1698 1725 1729 1730 1736 1738 1750 1752 1758 1763 1769 1783 1785 1792 1796 1824 1826 1834 1837 1839 1842 1843 1850 1856 1857 1862 1865 1869 1873 1875 1893 1894 1896 2037 2038 2059 2073 2078 2083 2087 2095 2148 2149 2157 2168 2169 2179 2184 2186 2194 2196 2386 2395 2397 2418 2419 2481 2487 2491 2495 2498 2517 2539 2549 2571 2593 2594 2618 2619 2638 2681 2683 2691 2697 2698 2715 2719 2730 2739 2748 2751 2769 2784 2791 2793 2796 2814 2830 2836 2841 2847 2849 2861 2863 2869 2870 2874 2894 2896 2914 2916 2917 2935 2937 2941 2945 2948 2950 2953 2954 2961 2967 2968 2971 2973 2976 2984 2986 3017 3019 3027 3028 3059 3071 3072 3078 3079 3082 3087 3091 3095 3097 3109 3148 3149 3167 3170 3176 3178 3184 3187 3189 3190 3194 3198 3207 3208 3259 3268 3270 3279 3280 3286 3295 3297 3418 3419 3491 3497 3509 3529 3592 3598 3617 3628 3671 3682 3701 3702 3708 3709 3710 3716 3718 3720 3729 3749 3761 3781 3792 3794 3802 3807 3814 3817 3819 3820 3826 3841 3859 3862 3870 3871 3891 3895 3901 3905 3907 3910 3914 3918 3925 3927 3941 3947 3950 3952 3958 3970 3972 3974 3981 3985 4016 4019 4061 4069 4091 4106 4109 4128 4129 4138 4139 4160 4182 4183 4189 4190 4192 4193 4198 4218 4219 4259 4278 4281 4287 4289 4291 4295 4298 4318 4319 4379 4381 4391 4397 4529 4592 4601 4609 4610 4728 4739 4782 4793 4812 4819 4821 4827 4829 4831 4872 4891 4892 4901 4906 4910 4912 4918 4921 4925 4928 4931 4937 4952 4960 4973 4981 4982 5017 5018 5029 5039 5071 5081 5092 5093 5127 5168 5170 5172 5178 5180 5186 5187 5209 5217 5239 5249 5271 5290 5293 5294 5309 5389 5390 5392 5398 5429 5492 5618 5681 5701 5710 5712 5718 5721 5781 5801 5810 5816 5817 5839 5861 5871 5893 5902 5903 5920 5923 5924 5930 5932 5938 5942 5983 6014 6019 6049 6091 6094 6104 6109 6128 6129 6137 6140 6158 6173 6179 6182 6185 6189 6190 6192 6197 6198 6218 6219 6238 6279 6281 6283 6289 6291 6297 6298 6328 6371 6382 6401 6409 6490 6518 6713 6719 6729 6731 6791 6792 6812 6815 6819 6823 6832 6851 6891 6892 6901 6904 6910 6912 6917 6918 6921 6927 6928 6940 6971 6972 6981 6982 7013 7015 7019 7023 7028 7031 7032 7038 7039 7051 7082 7083 7091 7093 7103 7105 7109 7125 7129 7130 7136 7138 7150 7152 7158 7163 7169 7183 7185 7190 7192 7196 7203 7208 7215 7219 7230 7239 7248 7251 7269 7280 7284 7291 7293 7296 7301 7302 7308 7309 7316 7318 7329 7349 7361 7380 7381 7390 7392 7394 7428 7439 7482 7493 7512 7518 7581 7613 7619 7629 7691 7692 7802 7803 7813 7820 7830 7831 7842 7851 7901 7903 7910 7916 7923 7926 7930 7932 7934 7943 7961 7962 8015 8023 8027 8037 8072 8073 8105 8124 8126 8134 8137 8139 8142 8143 8149 8150 8156 8157 8162 8165 8169 8193 8194 8196 8203 8207 8214 8216 8230 8236 8241 8247 8249 8261 8263 8269 8270 8274 8294 8296 8302 8307 8314 8319 8326 8341 8359 8362 8370 8371 8391 8395 8412 8419 8427 8429 8472 8491 8492 8517 8539 8561 8593 8612 8615 8619 8623 8629 8691 8692 8702 8703 8713 8715 8724 8913 8914 8916 8924 8926 8931 8935 8941 8942 8953 8961 8962 9013 9014 9016 9017 9025 9031 9035 9037 9041 9046 9052 9053 9061 9064 9071 9073 9103 9104 9106 9107 9124 9126 9127 9130 9134 9138 9140 9142 9143 9148 9160 9162 9167 9168 9170 9172 9176 9183 9184 9186 9205 9214 9216 9217 9235 9237 9241 9245 9248 9250 9253 9254 9261 9267 9268 9271 9273 9276 9284 9286 9301 9305 9307 9314 9318 9325 9327 9341 9347 9350 9352 9358 9370 9372 9374 9381 9385 9401 9406 9412 9413 9418 9425 9428 9437 9452 9460 9473 9481 9482 9502 9503 9523 9524 9538 9583 9601 9604 9612 9617 9618 9627 9628 9671 9672 9681 9682 9701 9703 9712 9716 9723 9726 9734 9813 9814 9816 9824 9826 9835

Python programs

Find common numbers in the OEIS

This program prints the numbers that occur in too many sequences in the OEIS. The output has already been copied to the main program below. To run this program, you need to download and extract the file stripped.gz first.

import collections
cntr = collections.Counter()
with open("stripped", "rt") as handle:
    handle.seek(0)
    for line in handle:
        line = line.rstrip("\n")
        if not line.startswith("#"):
            numbers = line.split(" ")[1]
            numbers = set(int(n) for n in numbers.split(",")[1:-1])
            cntr.update(n for n in numbers if 0 <= n <= 9999)
# numbers with repeating digits wouldn't be accepted anyway
print(sorted(
    n for n in cntr if cntr[n] >= 1000 and len(set(format(n, "04"))) == 4
))

Find English words

This program prints the list of English words. The output has already been copied to the main program below.

LETTERS1 = frozenset("abegiostz")  # 463910572
LETTERS2 = frozenset("abeglostz")  # 463910572
with open("/usr/share/dict/words", "rt") as handle:
    handle.seek(0)
    for line in handle:
        word = line.rstrip("\n")
        if len(word) == 4:
            set_ = set(word)
            if len(set_) == 4 and (
                set_.issubset(LETTERS1) or set_.issubset(LETTERS2)
            ):
                print(word, end=" ")
print()

The main program

This program prints the secure PINs under “The results” above.

import itertools

# OEIS A246547: p^n where p is a prime and n >= 2
PRIME_POWERS = frozenset((
    4, 8, 9, 16, 25, 27, 32, 49, 64, 81, 121, 125, 128, 169, 243, 256, 289,
    343, 361, 512, 529, 625, 729, 841, 961, 1024, 1331, 1369, 1681, 1849,
    2048, 2187, 2197, 2209, 2401, 2809, 3125, 3481, 3721, 4096, 4489, 4913,
    5041, 5329, 6241, 6561, 6859, 6889, 7921, 8192, 9409,
))

# numbers that occur in many OEIS sequences;
# copied from one of the previous programs
OEIS_COMMON = frozenset((
    123, 124, 125, 126, 127, 128, 129, 132, 134, 135, 136, 137, 138, 139, 142,
    143, 145, 146, 147, 148, 149, 152, 153, 154, 156, 157, 158, 159, 162, 163,
    164, 165, 167, 168, 169, 172, 173, 174, 175, 176, 178, 179, 182, 183, 184,
    185, 186, 187, 189, 192, 193, 194, 195, 196, 197, 198, 213, 214, 215, 216,
    217, 218, 219, 231, 234, 235, 236, 237, 238, 239, 241, 243, 245, 246, 247,
    248, 249, 251, 253, 254, 256, 257, 258, 259, 261, 263, 264, 265, 267, 268,
    269, 271, 273, 274, 275, 276, 278, 279, 281, 283, 284, 285, 286, 287, 289,
    291, 293, 294, 295, 296, 297, 298, 312, 314, 315, 316, 317, 318, 319, 321,
    324, 325, 326, 327, 328, 329, 341, 342, 345, 346, 347, 348, 349, 351, 352,
    354, 356, 357, 358, 359, 361, 362, 364, 365, 367, 368, 369, 371, 372, 374,
    375, 376, 378, 379, 381, 382, 384, 385, 386, 387, 389, 391, 392, 394, 395,
    396, 397, 398, 412, 413, 415, 416, 417, 418, 419, 421, 423, 425, 426, 427,
    428, 429, 431, 432, 435, 436, 437, 438, 439, 451, 452, 453, 456, 457, 458,
    459, 461, 462, 463, 465, 467, 468, 469, 471, 472, 473, 475, 476, 478, 479,
    481, 482, 483, 485, 486, 487, 489, 491, 492, 493, 495, 496, 497, 498, 512,
    513, 514, 516, 517, 518, 519, 521, 523, 524, 526, 527, 528, 529, 531, 532,
    534, 536, 539, 541, 546, 547, 549, 561, 563, 564, 567, 568, 569, 571, 572,
    576, 578, 587, 589, 593, 594, 612, 613, 617, 619, 624, 625, 629, 631, 637,
    641, 643, 645, 647, 648, 649, 651, 653, 659, 672, 673, 675, 683, 684, 691,
    693, 714, 715, 719, 721, 726, 728, 729, 735, 739, 741, 743, 751, 756, 761,
    768, 769, 784, 792, 816, 819, 821, 823, 825, 827, 829, 832, 839, 841, 853,
    857, 859, 863, 864, 896, 924, 937, 941, 945, 947, 953, 961, 967, 971, 972,
    983, 1023, 1024, 1025, 1039, 1049, 1056, 1063, 1069, 1087, 1089, 1093,
    1097, 1249, 1260, 1280, 1296, 1297, 1320, 1365, 1369, 1536, 1597, 1680,
    1728, 1764, 2016, 2048, 2187, 2304, 2310, 2401, 3125, 4096, 8192,
))

MAX_DAYS_PER_MONTH = {
    1: 31,
    2: 29,
    3: 31,
    4: 30,
    5: 31,
    6: 30,
    7: 31,
    8: 31,
    9: 30,
    10: 31,
    11: 30,
    12: 31,
}

MICROCHIPS = frozenset((
    # an incomplete list; many of these are from:
    #   https://en.wikipedia.org/wiki/Category:8-bit_microprocessors
    #   https://www.kingswood-consulting.co.uk/giicm/
    #   https://www.cpu-world.com/Support/
    # no need to add numbers that would be forbidden anyway
    #
    1673,  # PIC microcontroller
    2816,  # EEPROM
    6581,  # Commodore 64 SID (sound) chip
    6821,  # Motorola peripheral interface adapter
    6829,  # Motorola memory management unit
    7501,  # MOS CPU
    7815,  # voltage regulator
    7824,  # voltage regulator
    7912,  # voltage regulator
    8032,  # Intel microcontroller
    8051,  # Intel microcontroller
    8501,  # MOS CPU
    8516,  # Zilog DMA transfer controller
    8571,  # SRAM
))

DIGIT_TO_LETTER1 = {
    0: "o",
    1: "i",
    2: "z",
    3: "e",
    4: "a",
    5: "s",
    6: "g",
    7: "t",
    8: "b",
}
DIGIT_TO_LETTER2 = {
    0: "o",
    1: "l",  # "l" instead of "i"
    2: "z",
    3: "e",
    4: "a",
    5: "s",
    6: "g",
    7: "t",
    8: "b",
}

# English words; copied from one of the previous programs
ENGLISH_WORDS = frozenset((
    "abet", "able", "ages", "albs", "ales", "aloe", "also", "alto", "alts",
    "bags", "bait", "bale", "base", "bast", "bate", "bats", "beat", "begs",
    "belt", "best", "beta", "bets", "bias", "bite", "bits", "blat", "blog",
    "blot", "boas", "boat", "boga", "bogs", "bola", "bole", "bolt", "east",
    "eats", "egis", "egos", "gabs", "gait", "gale", "gals", "gate", "gaze",
    "gels", "gelt", "gets", "gibe", "gist", "glob", "goal", "goat", "gobs",
    "goes", "iota", "labs", "lags", "lase", "last", "late", "lats", "laze",
    "leas", "legs", "lest", "lets", "lobe", "lobs", "loge", "logs", "lose",
    "lost", "lots", "oats", "obit", "ogle", "sage", "sago", "sale", "salt",
    "sate", "seal", "seat", "site", "size", "slab", "slag", "slat", "slob",
    "sloe", "slog", "slot", "sole", "stab", "stag", "tabs", "tags", "tale",
    "teal", "teas", "ties", "toes", "toga", "togs", "zeal", "zest", "zeta",
    "zits",
))

def is_valid_pin(pin):
    # pin: tuple of 4 ints between 0-9
    # return: boolean

    zeroAsTen = tuple((d if d else 10) for d in pin)
    set_ = set(pin)  # distinct digits

    # all intervals between digits not distinct?
    if len(set(abs(a - b) for (a, b) in itertools.combinations(pin, r=2))) < 6:
        return False
    if len(
        set(abs(a - b) for (a, b) in itertools.combinations(zeroAsTen, r=2))
    ) < 6:
        return False

    # in increasing or decreasing order?
    if pin == tuple(sorted(pin)):
        return False
    if pin == tuple(sorted(pin, reverse=True)):
        return False
    if zeroAsTen == tuple(sorted(zeroAsTen)):
        return False
    if zeroAsTen == tuple(sorted(zeroAsTen, reverse=True)):
        return False

    int_ = int("".join(str(d) for d in pin))  # integer (0-9999)

    if int_ in PRIME_POWERS:  # a prime power?
        return False
    if int_ in OEIS_COMMON:  # in too many OEIS sequences?
        return False
    if 1908 <= int_ <= 2024:  # someone's birth year?
        return False

    # a date (MMDD or DDMM)?
    (firstHalf, secondHalf) = divmod(int_, 100)
    for (day, month) in ((firstHalf, secondHalf), (secondHalf, firstHalf)):
        if 1 <= month <= 12 and 1 <= day <= MAX_DAYS_PER_MONTH[month]:
            return False

    if int_ in MICROCHIPS:
        return False

    # looks like an English word?
    if "".join(DIGIT_TO_LETTER1.get(d, str(d)) for d in pin) in ENGLISH_WORDS:
        return False
    if "".join(DIGIT_TO_LETTER2.get(d, str(d)) for d in pin) in ENGLISH_WORDS:
        return False

    return True

def main():
    cnt = 0
    for pin in itertools.product(range(10), repeat=4):
        if is_valid_pin(pin):
            print("".join(str(d) for d in pin), end=" ")
            cnt += 1
    print()
    print("Count:", cnt)

main()