[{"element_type":"user_image","content":"https://cdn-learn.adafruit.com/user_assets/assets/000/002/085/original/neokey-totp-token-demo.jpeg?1765135614","metadata":{}},{"element_type":"text","content":"\n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n        \u003ch2\u003eOverview\u003c/h2\u003e\n      \n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n","metadata":{}},{"element_type":"text","content":"\n  \n  \n  \n  \n  \n  \n        \u003cp\u003eThis is a two factor authentication token to generate TOTP login codes for up to four accounts. You can select which account by pressing a key on the 4-key NeoKey keypad. The design is intended for desktop use in a safe location (wall power + no worries of physical tampering) but where you do want to prevent secrets from leaking over the network due to mis-configured cloud-sync backup features or whatever.\u003c/p\u003e\n\u003cp\u003eDesign Goals and Features:\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003eMake the codes really easy to read and type, even in low light, by using a\u0026nbsp;dimmable backlit TFT display with a relatively large font.\u003c/li\u003e\n\u003cli\u003e\u003cspan\u003eSupport 4 TOTP account slots (one for each key of a 4-key NeoKey keypad).\u003c/span\u003e\u003c/li\u003e\n\u003cli\u003eThe NeoPixel under the key for the currently selected account slot lights up. Pressing a different key switches the selected account. Pressing the selected key a second time puts the token in standby mode (backlight and NeoPixels off).\u003c/li\u003e\n\u003cli\u003eStore secrets in an I2C EEPROM rather than in the CLUE board's flash. This makes it so the secrets aren't trivially accessible to a connected computer as USB mass storage files. This way, they won't get accidentally sucked into backups, and malware would have to work harder to access them.\u003c/li\u003e\n\u003cli\u003eSet DS3231 RTC time from the USB serial console by opening the REPL, importing the \u003ccode\u003eutil\u003c/code\u003e\u0026nbsp;module, then calling \u003ccode\u003eutil.set_time()\u003c/code\u003e.\u003c/li\u003e\n\u003cli\u003eAdd and manage TOTP accounts in the EEPROM's database of account slots by using similar REPL functions (\u003ccode\u003eimport util\u003c/code\u003e then \u003ccode\u003eutil.menu()\u003c/code\u003e).\u003c/li\u003e\n\u003cli\u003eUse the token fully airgapped after initial setup by powering it from a phone charger and reading codes off the TFT display.\u003c/li\u003e\n\u003c/ol\u003e\n      \n\n\n\n\n\n","metadata":{}},{"element_type":"text","content":"\n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n        \u003ch2\u003eParts\u003c/h2\u003e\n      \n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n","metadata":{}},{"element_type":"user_image","content":"https://cdn-learn.adafruit.com/user_assets/assets/000/002/086/original/neokey-totp-token-assembly.jpeg?1765136569","metadata":{}},{"element_type":"product","content":"https://www.adafruit.com/product/4500","metadata":{}},{"element_type":"product","content":"https://www.adafruit.com/product/4980","metadata":{}},{"element_type":"product","content":"https://www.adafruit.com/product/4954","metadata":{}},{"element_type":"product","content":"https://www.adafruit.com/product/5008","metadata":{}},{"element_type":"product","content":"https://www.adafruit.com/product/5188","metadata":{}},{"element_type":"product","content":"https://www.adafruit.com/product/380","metadata":{}},{"element_type":"product","content":"https://www.adafruit.com/product/5146","metadata":{}},{"element_type":"product","content":"https://www.adafruit.com/product/4399","metadata":{}},{"element_type":"product","content":"https://www.adafruit.com/product/4210","metadata":{}},{"element_type":"parts","content":"\n  \u003cdiv class=\"parts-details\"\u003e\n    \u003cdiv\u003e\n      \u003cspan class=\"parts-quantity\"\u003e1\u003c/span\u003e\u003cspan\u003e x \u003c/span\u003e\n        \u003ca href=\"https://www.amazon.com/dp/B00GDXTYT6\" class=\"parts-name\" target=\"_blank\"\u003e#2-28x316\" Thread Rolling Screws\u003c/a\u003e\n      \u003cdiv class=\"parts-description\"\u003eSmall Parts 0203LPP Steel Thread Rolling Screw for Plastic, Zinc Plated, Pan Head, Phillips Drive, #2-28 Thread Size, 3/16\" Length (Pack of 100)\u003c/div\u003e\n    \u003c/div\u003e\n  \u003c/div\u003e\n  \u003cdiv class=\"parts-action\"\u003e\n  \u003cdiv\u003e\n      \u003ca href=\"https://www.amazon.com/dp/B00GDXTYT6\" class=\"parts-url btn-primary\" target=\"_blank\"\u003eBuy Now\u003c/a\u003e\n\n  \u003c/div\u003e\n\u003c/div\u003e\n\n  \u003cdiv class=\"clearfix\"\u003e\u003c/div\u003e\n","metadata":{"name":"#2-28x316\" Thread Rolling Screws","url":"https://www.amazon.com/dp/B00GDXTYT6","description":"Small Parts 0203LPP Steel Thread Rolling Screw for Plastic, Zinc Plated, Pan Head, Phillips Drive, #2-28 Thread Size, 3/16\" Length (Pack of 100)","quantity":"1"}},{"element_type":"parts","content":"\n  \u003cdiv class=\"parts-details\"\u003e\n    \u003cdiv\u003e\n      \u003cspan class=\"parts-quantity\"\u003e1\u003c/span\u003e\u003cspan\u003e x \u003c/span\u003e\n        \u003ca href=\"https://www.amazon.com/dp/B00GDYG5CE\" class=\"parts-name\" target=\"_blank\"\u003e#2-28x1/4\" Thread Rolling Screws\u003c/a\u003e\n      \u003cdiv class=\"parts-description\"\u003eSmall Parts 0204LPPBZ Steel Thread Rolling Screw for Plastic, Black Zinc Plated, Pan Head, Phillips Drive, #2-28 Thread Size, 1/4\" Length (Pack of 100)\u003c/div\u003e\n    \u003c/div\u003e\n  \u003c/div\u003e\n  \u003cdiv class=\"parts-action\"\u003e\n  \u003cdiv\u003e\n      \u003ca href=\"https://www.amazon.com/dp/B00GDYG5CE\" class=\"parts-url btn-primary\" target=\"_blank\"\u003eBuy Now\u003c/a\u003e\n\n  \u003c/div\u003e\n\u003c/div\u003e\n\n  \u003cdiv class=\"clearfix\"\u003e\u003c/div\u003e\n","metadata":{"name":"#2-28x1/4\" Thread Rolling Screws","url":"https://www.amazon.com/dp/B00GDYG5CE","description":"Small Parts 0204LPPBZ Steel Thread Rolling Screw for Plastic, Black Zinc Plated, Pan Head, Phillips Drive, #2-28 Thread Size, 1/4\" Length (Pack of 100)","quantity":"1"}},{"element_type":"text","content":"\n  \n  \n  \n  \n        \u003ch2\u003e3D Printable Enclosure\u003c/h2\u003e\n\u003cp\u003eYou can download the Blender design file (.blend) and printable STL files for the enclosure with the buttons below. I designed these to print on a Bambu Lab P1S with the standard 0.2mm layer height preset. I didn't do anything special other than calibrating my X-Y hole compensation, drying my filament, and slowing down the maximum acceleration.\u003c/p\u003e\n\u003cp\u003eThe enclosure is designed with a cable slot so you can plug a microUSB cable into the CLUE, then screw the bezel onto the base.\u003c/p\u003e\n\u003cp\u003e\u003cspan\u003eTo attach the I2C breakout boards to the base of the enclosure, use 2-28x3/16\" thread rolling screws. To attach the bezel to the base, use 2-28x1/4\" thread rolling screws.\u003c/span\u003e\u003c/p\u003e\n\u003cp\u003eTo access the reset button, stick a toothpick into the hole in the bottom of the base.\u003c/p\u003e\n      \n\n\n\n","metadata":{}},{"element_type":"button","content":"https://github.com/samblenny/neokey-totp-token/releases/download/v0.3.1/neokey-clue-totp-nopcb.blend","metadata":{"button_type":"primary","icon":"","target":"_blank","zip_folder":"false"}},{"element_type":"button","content":"https://github.com/samblenny/neokey-totp-token/releases/download/v0.3.1/neokey-clue-totp-base.stl","metadata":{"button_type":"primary","icon":"","target":"_blank","zip_folder":"false"}},{"element_type":"button","content":"https://github.com/samblenny/neokey-totp-token/releases/download/v0.3.1/neokey-clue-totp-bezel.stl","metadata":{"button_type":"primary","icon":"","target":"_blank","zip_folder":"false"}},{"element_type":"text","content":"\n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n        \u003ch2\u003eCircuitPython Code\u003c/h2\u003e\n\u003cp\u003eYou can view the code at the\u003cspan\u003e\u0026nbsp;\u003ca href=\"https://github.com/samblenny/neokey-totp-token\" target=\"_blank\"\u003esamblenny/neokey-totp-token\u003c/a\u003e\u0026nbsp;\u003c/span\u003eGitHub repository. To download a zip archive project bundle with the code and all the necessary libraries, use the \"Download Project Bundle\" button:\u003c/p\u003e\n      \n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n","metadata":{}},{"element_type":"button","content":"https://github.com/samblenny/neokey-totp-token/releases/download/v0.3.1/neokey-totp-token-2a1b330.zip","metadata":{"button_type":"primary","icon":"","target":"_blank","zip_folder":"false"}},{"element_type":"text","content":"\n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n        \u003ch2\u003eInstall \u0026amp; Setup\u003c/h2\u003e\n      \n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n","metadata":{}},{"element_type":"markdown","content":"\u003col\u003e\n\u003cli\u003e\u003cp\u003eEnsure your CLUE board has an \u003ca href=\"https://learn.adafruit.com/adafruit-clue/update-bootloader\"\u003eup to date bootloader\u003c/a\u003e\u003c/p\u003e\u003c/li\u003e\n\u003cli\u003e\u003cp\u003eDownload the latest version of the CircuitPython 10 UF2 file from the circuitpython.org downloads page for \u003ca href=\"https://circuitpython.org/board/clue_nrf52840_express/\"\u003eCLUE NRF52840 Express\u003c/a\u003e. At the time I'm writing this (Dec 9, 2025), the current version is 10.0.3, but a PR just merged with a faster hashlib.sha1 implementation. If you want faster sha1, you can try the BROWSE S3 download button under Absolute Newest and look for \u003ccode class=\"inline\"\u003eadafruit-circuitpython-clue_nrf52840_express-en_US-20251209-main-PR10740-648aae0.uf2\u003c/code\u003e or newer. Once 10.1.0 gets released, just use the normal stable release version.\u003c/p\u003e\u003c/li\u003e\n\u003cli\u003e\u003cp\u003eFollow Adafruit's\n\u003ca href=\"https://learn.adafruit.com/adafruit-clue/circuitpython\"\u003eCircuitPython on CLUE\u003c/a\u003e\nLearn guide to install CircuitPython: plug CLUE into a computer, double-click\nCLUE's reset button, wait for CLUEBOOT drive, drag UF2 file onto CLUEBOOT\ndrive, wait for copy to finish. Be sure to use a USB data cable (charge-only cables won't work! ). If you have already assembled the 3D printed enclosure, use a toothpick through the hole on the bottom to access the reset button.\u003c/p\u003e\u003c/li\u003e\n\u003cli\u003e\u003cp\u003eUnplug the CLUE and assemble the rest of the hardware.\u003c/p\u003e\u003c/li\u003e\n\u003cli\u003e\u003cp\u003eGet the project bundle from the \"Download Project Bundle\" button below and\ncopy its code and library files from the 10.x directory to your CLUE's\n\u003ccode class=\"inline\"\u003eCIRCUITPY\u003c/code\u003e drive.\u003c/p\u003e\u003c/li\u003e\n\u003c/ol\u003e\n","metadata":{"markdown":"1. Ensure your CLUE board has an [up to date bootloader](https://learn.adafruit.com/adafruit-clue/update-bootloader)\n\n2. Download the latest version of the CircuitPython 10 UF2 file from the circuitpython.org downloads page for [CLUE NRF52840 Express](https://circuitpython.org/board/clue_nrf52840_express/). At the time I'm writing this (Dec 9, 2025), the current version is 10.0.3, but a PR just merged with a faster hashlib.sha1 implementation. If you want faster sha1, you can try the BROWSE S3 download button under Absolute Newest and look for `adafruit-circuitpython-clue_nrf52840_express-en_US-20251209-main-PR10740-648aae0.uf2` or newer. Once 10.1.0 gets released, just use the normal stable release version.\n\n3. Follow Adafruit's\n   [CircuitPython on CLUE](https://learn.adafruit.com/adafruit-clue/circuitpython)\n   Learn guide to install CircuitPython: plug CLUE into a computer, double-click\n   CLUE's reset button, wait for CLUEBOOT drive, drag UF2 file onto CLUEBOOT\n   drive, wait for copy to finish. Be sure to use a USB data cable (charge-only cables won't work! ). If you have already assembled the 3D printed enclosure, use a toothpick through the hole on the bottom to access the reset button.\n\n4. Unplug the CLUE and assemble the rest of the hardware.\n\n5. Get the project bundle from the \"Download Project Bundle\" button below and\n    copy its code and library files from the 10.x directory to your CLUE's\n    `CIRCUITPY` drive.\n"}},{"element_type":"text","content":"\n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n        \u003ch3\u003eInstall Project Bundle\u003c/h3\u003e\n\u003cp class=\"p2\"\u003e\u003cspan class=\"s1\"\u003eTo copy the project bundle files to your CIRCUITPY drive:\u003c/span\u003e\u003c/p\u003e\n\u003col class=\"ol1\"\u003e\n\u003cli class=\"li3\"\u003e\u003cspan class=\"s1\"\u003eDownload the project bundle .zip file using the \"Download Project Bundle\" button above.\u003c/span\u003e\u003c/li\u003e\n\u003cli class=\"li3\"\u003e\u003cspan class=\"s1\"\u003eExpand the zip file by opening it, or use unzip in a Terminal. The zip archive should expand to a folder. When you open the folder, it should contain a README.txt file and a CircuitPython 10.x folder.\u003c/span\u003e\u003c/li\u003e\n\u003cli class=\"li3\"\u003e\u003cspan class=\"s1\"\u003eOpen the CircuitPython 10.x folder and copy all of its contents to your CIRCUITPY drive.\u003c/span\u003e\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp class=\"p2\"\u003e\u003cspan class=\"s1\"\u003eTo learn more about copying libraries to your\u003cspan\u003e\u0026nbsp;\u003c/span\u003e\u003cstrong\u003eCIRCUITPY\u003c/strong\u003e\u003cspan\u003e\u0026nbsp;\u003c/span\u003edrive, check out the\u0026nbsp;\u003ca href=\"https://learn.adafruit.com/welcome-to-circuitpython/circuitpython-libraries\" target=\"_blank\"\u003e\u003cspan class=\"s2\"\u003eCircuitPython Libraries\u003c/span\u003e\u003c/a\u003e\u0026nbsp;section of the\u0026nbsp;\u003ca href=\"https://learn.adafruit.com/welcome-to-circuitpython\" target=\"_blank\"\u003e\u003cspan class=\"s2\"\u003eWelcome to CircuitPython!\u003c/span\u003e\u003c/a\u003e\u0026nbsp;learn guide.\u003c/span\u003e\u003c/p\u003e\n      \n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n","metadata":{}},{"element_type":"text","content":"\n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n        \u003ch3\u003eSet RTC Time in UTC\u003c/h3\u003e\n      \n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n","metadata":{}},{"element_type":"markdown","content":"\u003cp\u003eWhen you first install a battery in the DS3231, the time will be wildly wrong.\nYou can set the time from the REPL by importing the \u003ccode class=\"inline\"\u003eutil\u003c/code\u003e module then calling\n\u003ccode class=\"inline\"\u003eutil.set_time()\u003c/code\u003e, like this:\u003c/p\u003e\n\u003ccode class=\"\"\u003e\u003cpre\u003eAdafruit CircuitPython 10.0.3 on 2025-10-17; Adafruit CLUE nRF52840 Express with nRF52840\n\u0026gt;\u0026gt;\u0026gt; import util\n\u0026gt;\u0026gt;\u0026gt; util.now()\n'2000-01-01 02:06:52'\n\u0026gt;\u0026gt;\u0026gt; util.set_time()\nSet DS3231 RTC time...\n   year: 2025\n  month: 11\n    day: 25\n   hour: 00\n minute: 01\nseconds: 02\nnew RTC time:  2025-11-25 00:01:02\n\u0026gt;\u0026gt;\u0026gt; # wait a while, then check the time\n\u0026gt;\u0026gt;\u0026gt; util.get_time()\n2025-11-25 00:06:02\n\u0026gt;\u0026gt;\u0026gt;\n\u003c/pre\u003e\u003c/code\u003e\n\u003cp\u003eWhen you set the time, use a GPS or NTP synchronized reference that shows UTC\ntime with seconds (e.g. \u003ca href=\"https://samblenny.github.io/totp-util/clock/\"\u003ehttps://samblenny.github.io/totp-util/clock/\u003c/a\u003e ). Don't\nset the clock according to your local timezone. You must use UTC.\u003c/p\u003e\n\n\u003cp\u003eEach of the \"year:\", \"month:\", \"day:\", etc. prompts come from python's\n\u003ccode class=\"inline\"\u003einput()\u003c/code\u003e function. The text on the left is the input prompt shown by\n\u003ccode class=\"inline\"\u003eset_time()\u003c/code\u003e, and the numbers on the right are an example of what you might\ntype for your response. When you hit return or enter for the seconds line, the\ntime will be set immediately.\u003c/p\u003e\n","metadata":{"markdown":"When you first install a battery in the DS3231, the time will be wildly wrong.\nYou can set the time from the REPL by importing the `util` module then calling\n`util.set_time()`, like this:\n\n```\nAdafruit CircuitPython 10.0.3 on 2025-10-17; Adafruit CLUE nRF52840 Express with nRF52840\n\u0026gt;\u0026gt;\u0026gt; import util\n\u0026gt;\u0026gt;\u0026gt; util.now()\n'2000-01-01 02:06:52'\n\u0026gt;\u0026gt;\u0026gt; util.set_time()\nSet DS3231 RTC time...\n   year: 2025\n  month: 11\n    day: 25\n   hour: 00\n minute: 01\nseconds: 02\nnew RTC time:  2025-11-25 00:01:02\n\u0026gt;\u0026gt;\u0026gt; # wait a while, then check the time\n\u0026gt;\u0026gt;\u0026gt; util.get_time()\n2025-11-25 00:06:02\n\u0026gt;\u0026gt;\u0026gt;\n```\n\nWhen you set the time, use a GPS or NTP synchronized reference that shows UTC\ntime with seconds (e.g. https://samblenny.github.io/totp-util/clock/ ). Don't\nset the clock according to your local timezone. You must use UTC.\n\nEach of the \"year:\", \"month:\", \"day:\", etc. prompts come from python's\n`input()` function. The text on the left is the input prompt shown by\n`set_time()`, and the numbers on the right are an example of what you might\ntype for your response. When you hit return or enter for the seconds line, the\ntime will be set immediately."}},{"element_type":"text","content":"\n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n        \u003ch3\u003eStore TOTP Accounts in EEPROM Database\u003c/h3\u003e\n      \n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n","metadata":{}},{"element_type":"markdown","content":"\u003cp\u003eThe \u003ccode class=\"inline\"\u003eutil\u003c/code\u003e module has several functions for managing the 4KB EEPROM as a\ndatabase of TOTP account details. You can enter QR code URIs with a 2D barcode\nreader or by decoding elsewhere then copying and pasting. The EEPROM database\nonly stores a short (8 character) label and the 32 byte decoded seed (from the\n\u003ccode class=\"inline\"\u003e\u0026amp;secret=...\u003c/code\u003e TOTP URI query parameter). The only supported options for\n\u003ccode class=\"inline\"\u003e\u0026amp;algorithm=\u003c/code\u003e, \u003ccode class=\"inline\"\u003e\u0026amp;digits=\u003c/code\u003e, and \u003ccode class=\"inline\"\u003e\u0026amp;period=\u003c/code\u003e are SHA1, 6 digits, and 30 seconds.\nThat will work for many services that use TOTP. If you need something else, you\ncan modify the code.\u003c/p\u003e\n\n\u003cp\u003eTo get started managing the EEPROM database, open the USB serial console and do\nthe Ctrl-C thing to get into the CircuitPython REPL. From there, \u003ccode class=\"inline\"\u003eimport util\u003c/code\u003e\nthen call \u003ccode class=\"inline\"\u003eutil.menu()\u003c/code\u003e to see what's available. The first time, \u003cstrong\u003eyou will need\nto format the EEPROM\u003c/strong\u003e. After that, you can add, erase, or copy accounts as you\nlike. There are 4 available slots (one for each key of a 4-key NeoKey keypad).\u003c/p\u003e\n\n\u003cp\u003eThis is an example of how it looks to use the menu and provision two demo\naccounts (I used a USB 2D barcode reader to type the otpauth URIs):\u003c/p\u003e\n\u003ccode class=\"\"\u003e\u003cpre\u003e\u0026gt;\u0026gt;\u0026gt; import util\n\u0026gt;\u0026gt;\u0026gt; util.menu()\nAvailable functions:\n\n 1. add_totp_account()    - Add a new TOTP account.\n 2. copy_totp_account()   - Copy a TOTP account to another slot.\n 3. erase_totp_account()  - Erase a TOTP account from EEPROM.\n 4. format_eeprom()       - Format the EEPROM for TOTP storage.\n 5. get_time()            - Get the current DS3231 RTC time.\n 6. list_totp_accounts()  - List all stored TOTP accounts.\n 7. set_time()            - Set the DS3231 RTC time.\n\nChoose a function by number (or Enter to cancel): 4\nAre you sure? This will erase all data. (y/n): y\nWriting header page...\nZeroing 127 pages:\n................................................................\n...............................................................\nEEPROM formatted successfully.\n\u0026gt;\u0026gt;\u0026gt; util.add_totp_account()\nEnter slot number (1-4): 1\nEnter label (max 8 chars): Alice\nEnter TOTP URI: otpauth://totp/Example:alice@google.com?secret=JBSWY3DPEHPK3PXP\u0026amp;issuer=Example\nRecord added to slot 1.\n\u0026gt;\u0026gt;\u0026gt; util.add_totp_account()\nEnter slot number (1-4): 2\nEnter label (max 8 chars): JohnDoe\nEnter TOTP URI: otpauth://totp/ACME%20Co:john.doe@email.com?secret=HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ\u0026amp;issuer=ACME%20Co\u0026amp;algorithm=SHA1\u0026amp;digits=6\u0026amp;period=30\nRecord added to slot 2.\n\u0026gt;\u0026gt;\u0026gt; util.list_totp_accounts()\nSlot 1: 'Alice'\nSlot 2: 'JohnDoe'\nSlot 3: -- empty --\nSlot 4: -- empty --\n\u0026gt;\u0026gt;\u0026gt;\n\u003c/pre\u003e\u003c/code\u003e","metadata":{"markdown":"The `util` module has several functions for managing the 4KB EEPROM as a\ndatabase of TOTP account details. You can enter QR code URIs with a 2D barcode\nreader or by decoding elsewhere then copying and pasting. The EEPROM database\nonly stores a short (8 character) label and the 32 byte decoded seed (from the\n`\u0026amp;secret=...` TOTP URI query parameter). The only supported options for\n`\u0026amp;algorithm=`, `\u0026amp;digits=`, and `\u0026amp;period=` are SHA1, 6 digits, and 30 seconds.\nThat will work for many services that use TOTP. If you need something else, you\ncan modify the code.\n\nTo get started managing the EEPROM database, open the USB serial console and do\nthe Ctrl-C thing to get into the CircuitPython REPL. From there, `import util`\nthen call `util.menu()` to see what's available. The first time, **you will need\nto format the EEPROM**. After that, you can add, erase, or copy accounts as you\nlike. There are 4 available slots (one for each key of a 4-key NeoKey keypad).\n\nThis is an example of how it looks to use the menu and provision two demo\naccounts (I used a USB 2D barcode reader to type the otpauth URIs):\n\n```\n\u0026gt;\u0026gt;\u0026gt; import util\n\u0026gt;\u0026gt;\u0026gt; util.menu()\nAvailable functions:\n\n 1. add_totp_account()    - Add a new TOTP account.\n 2. copy_totp_account()   - Copy a TOTP account to another slot.\n 3. erase_totp_account()  - Erase a TOTP account from EEPROM.\n 4. format_eeprom()       - Format the EEPROM for TOTP storage.\n 5. get_time()            - Get the current DS3231 RTC time.\n 6. list_totp_accounts()  - List all stored TOTP accounts.\n 7. set_time()            - Set the DS3231 RTC time.\n\nChoose a function by number (or Enter to cancel): 4\nAre you sure? This will erase all data. (y/n): y\nWriting header page...\nZeroing 127 pages:\n................................................................\n...............................................................\nEEPROM formatted successfully.\n\u0026gt;\u0026gt;\u0026gt; util.add_totp_account()\nEnter slot number (1-4): 1\nEnter label (max 8 chars): Alice\nEnter TOTP URI: otpauth://totp/Example:alice@google.com?secret=JBSWY3DPEHPK3PXP\u0026amp;issuer=Example\nRecord added to slot 1.\n\u0026gt;\u0026gt;\u0026gt; util.add_totp_account()\nEnter slot number (1-4): 2\nEnter label (max 8 chars): JohnDoe\nEnter TOTP URI: otpauth://totp/ACME%20Co:john.doe@email.com?secret=HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ\u0026amp;issuer=ACME%20Co\u0026amp;algorithm=SHA1\u0026amp;digits=6\u0026amp;period=30\nRecord added to slot 2.\n\u0026gt;\u0026gt;\u0026gt; util.list_totp_accounts()\nSlot 1: 'Alice'\nSlot 2: 'JohnDoe'\nSlot 3: -- empty --\nSlot 4: -- empty --\n\u0026gt;\u0026gt;\u0026gt;\n```"}},{"element_type":"text","content":"\n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n        \u003ch2\u003eUsing TOTP Codes\u003c/h2\u003e\n\u003cp\u003eOnce the TOTP token is provisioned with accounts, plug it into a phone charger for power. At power up, the screen will turn on and start showing TOTP codes for the account in slot 1. To switch to a different account slot, press one of the other NeoKeys. To turn off the screen and put the token in standby mode, press the key for the currently selected slot.\u003c/p\u003e\n\u003ch3\u003eReading the Display\u003c/h3\u003e\n\u003cp\u003eFor slots that have an account provisioned, the display has 3 lines:\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003eFirst line is seconds modulo 30. Once you see this get above 20, type fast or wait for the code to change after 29.\u003c/li\u003e\n\u003cli\u003eSecond line has the label that you set for the selected slot when you provisioned it with `util.add_totp_account()` in the REPL.\u003c/li\u003e\n\u003cli\u003eThird line has the TOTP code. This changes every 30 seconds.\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003eFor empty slots, the label line will say \"empty\" and the TOTP code line will be blank.\u003c/p\u003e\n      \n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n","metadata":{}},{"element_type":"text","content":"\n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n        \u003ch2\u003eCircuitPython Code\u003c/h2\u003e\n\u003ch3\u003eCIRCUITPY/code.py\u003c/h3\u003e\n      \n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n","metadata":{}},{"element_type":"embed","content":"https://github.com/samblenny/neokey-totp-token/blob/v0.3.1/code.py"},{"element_type":"text","content":"\n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n        \u003ch3\u003eCIRCUITPY/boot.py\u003c/h3\u003e\n      \n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n","metadata":{}},{"element_type":"embed","content":"https://github.com/samblenny/neokey-totp-token/blob/v0.3.1/boot.py"},{"element_type":"text","content":"\n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n        \u003ch3\u003eCIRCUITPY/controller.py\u003c/h3\u003e\n      \n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n","metadata":{}},{"element_type":"embed","content":"https://github.com/samblenny/neokey-totp-token/blob/v0.3.1/controller.py"},{"element_type":"text","content":"\n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n        \u003ch3\u003eCIRCUITPY/eeprom_db.py\u003c/h3\u003e\n      \n\n\n\n\n\n\n\n\n\n\n\n\n\n\n","metadata":{}},{"element_type":"embed","content":"https://github.com/samblenny/neokey-totp-token/blob/v0.3.1/eeprom_db.py"},{"element_type":"text","content":"\n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n        \u003ch3\u003eCIRCUITPY/sb_hmac.py\u003c/h3\u003e\n      \n\n\n\n\n\n\n\n\n\n\n\n\n","metadata":{}},{"element_type":"embed","content":"https://github.com/samblenny/neokey-totp-token/blob/v0.3.1/sb_hmac.py"},{"element_type":"text","content":"\n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n        \u003ch3\u003eCIRCUITPY/sb_totp.py\u003c/h3\u003e\n      \n\n\n\n\n\n\n\n\n\n\n","metadata":{}},{"element_type":"embed","content":"https://github.com/samblenny/neokey-totp-token/blob/v0.3.1/sb_totp.py"},{"element_type":"text","content":"\n  \n  \n  \n  \n  \n  \n  \n  \n  \n        \u003ch3\u003eCIRCUITPY/util.py\u003c/h3\u003e\n      \n\n\n\n\n\n\n\n\n","metadata":{}},{"element_type":"embed","content":"https://github.com/samblenny/neokey-totp-token/blob/v0.3.1/util.py"}]