You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
trezor-firmware/python/tools/trezor-otp.py

119 lines
3.5 KiB

#!/usr/bin/env python3
# This file is part of the Trezor project.
#
# Copyright (C) 2012-2022 SatoshiLabs and contributors
#
# This library is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License version 3
# as published by the Free Software Foundation.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the License along with this library.
# If not, see <https://www.gnu.org/licenses/lgpl-3.0.html>.
import configparser
import os
import re
import sys
import pyotp
from trezorlib.client import TrezorClient
from trezorlib.misc import decrypt_keyvalue, encrypt_keyvalue
from trezorlib.tools import parse_path
from trezorlib.transport import get_transport
from trezorlib.ui import ClickUI
BIP32_PATH = parse_path("10016h/0")
def encrypt(type: str, domain: str, secret: str) -> str:
transport = get_transport()
client = TrezorClient(transport, ClickUI())
dom = type.upper() + ": " + domain
enc = encrypt_keyvalue(client, BIP32_PATH, dom, secret.encode(), False, True)
client.close()
return enc.hex()
def decrypt(type: str, domain: str, secret: bytes) -> bytes:
transport = get_transport()
client = TrezorClient(transport, ClickUI())
dom = type.upper() + ": " + domain
dec = decrypt_keyvalue(client, BIP32_PATH, dom, secret, False, True)
client.close()
return dec
class Config:
def __init__(self) -> None:
XDG_CONFIG_HOME = os.getenv("XDG_CONFIG_HOME", os.path.expanduser("~/.config"))
os.makedirs(XDG_CONFIG_HOME, exist_ok=True)
self.filename = XDG_CONFIG_HOME + "/trezor-otp.ini"
self.config = configparser.ConfigParser()
self.config.read(self.filename)
def add(self, domain: str, secret: str, type: str = "totp") -> None:
self.config[domain] = {}
self.config[domain]["secret"] = encrypt(type, domain, secret)
self.config[domain]["type"] = type
if type == "hotp":
self.config[domain]["counter"] = "0"
with open(self.filename, "w") as f:
self.config.write(f)
def get(self, domain: str):
s = self.config[domain]
if s["type"] == "hotp":
s["counter"] = str(int(s["counter"]) + 1)
with open(self.filename, "w") as f:
self.config.write(f)
secret = decrypt(s["type"], domain, bytes.fromhex(s["secret"]))
if s["type"] == "totp":
return pyotp.TOTP(secret).now()
if s["type"] == "hotp":
c = int(s["counter"])
return pyotp.HOTP(secret).at(c)
return ValueError("unknown domain or type")
def add() -> None:
c = Config()
domain = input("domain: ")
while True:
secret = input("secret: ")
if re.match(r"^[A-Z2-7]{16}$", secret):
break
print("invalid secret")
while True:
type = input("type (t=totp h=hotp): ")
if type in ("t", "h"):
break
print("invalid type")
c.add(domain, secret, type + "otp")
print("Entry added")
def get(domain: str) -> None:
c = Config()
s = c.get(domain)
print(s)
def main() -> None:
if len(sys.argv) < 2:
print("Usage: trezor-otp.py [add|domain]")
sys.exit(1)
if sys.argv[1] == "add":
add()
else:
get(sys.argv[1])
if __name__ == "__main__":
main()