Witam mam pytanie jak stworzyć swój generator tokenów? Czy tak się da?
Biorąc pod uwagę, że ktoś kiedyś stworzył… to tak, da się.
Jak chcesz gotowe algorytmy, to wyszukaj sobie w sieci TOTP (Time-based One-Time Password), HOTP (HMAC-based One-Time Password) czy OCRA (OATH Challenge-Response Algorithm).
Jak chcesz ukuć coś swojego, to najprostsza idea jest taka, żeby mieć jakiś sekret, jakąś sól i jakąś funkcję skrótu, i liczyć ją z tego sekretu i soli. Adaptujesz ten schemat wg potrzeb — np. jak chcesz mieć kupony zniżkowe, to sekret trzymasz tylko na serwerze, a sól to niech będzie generowany losowy ciąg n
znaków; dajesz użytkownikom n + hasz(n + sekret)
, a potem na serwerze odcinasz pierwszych n
znaków otrzymanego kodu, liczysz z niego ten hasz i patrzysz, czy się zgadza. Albo jak chcesz czasowe tokeny, to niech sekret będzie wspólny dla użytkownika i serwera, a sól to czas.
Szkic rozwiązania. Prawie gotowiec, ale święta i w ogóle, to nawet Althorion potrafi przemówić ludzkim głosem:
from hashlib import blake2b
from random import randint
COUNTER_LENGTH_IN_CHARS = 6 # arbitrary, gives 16 ** COUNTER_LENGTH_IN_CHARS possible tokens
HASH_LENGTH_IN_BYTES = 6 # arbitrary <= 64 == blake2b.MAX_DIGEST_SIZE
assert HASH_LENGTH_IN_BYTES <= blake2b.MAX_DIGEST_SIZE
HASH_LENGTH_IN_CHARS = HASH_LENGTH_IN_BYTES * 2
TOKEN_LENGTH_IN_CHARS = COUNTER_LENGTH_IN_CHARS + HASH_LENGTH_IN_CHARS
SECRET_HASH_KEY = b"much secret, very hash, WOW!" # probably want something with higher entropy than that
assert len(SECRET_HASH_KEY) <= blake2b.MAX_KEY_SIZE
def generate_token(secret: bytes = SECRET_HASH_KEY) -> str:
salt_value = randint(0, 16 ** COUNTER_LENGTH_IN_CHARS - 1)
salt_string = f"{salt_value:06x}"
h = blake2b(digest_size=HASH_LENGTH_IN_BYTES, key=secret)
h.update(bytes(salt_string, encoding="ascii"))
return f"{salt_string}{h.hexdigest()}"
def validate_token(token: str, secret: bytes = SECRET_HASH_KEY) -> bool:
if len(token) != TOKEN_LENGTH_IN_CHARS:
return False
salt_string = token[:COUNTER_LENGTH_IN_CHARS]
expected_hash = token[COUNTER_LENGTH_IN_CHARS:]
h = blake2b(digest_size=HASH_LENGTH_IN_BYTES, key=secret)
h.update(bytes(salt_string, encoding="ascii"))
return expected_hash == h.hexdigest()
if __name__ == "__main__":
assert validate_token("cedabf13e7fec65ec3") # known good token
assert validate_token("a3705e4692be865b7c") is False # known bad token
N_OF_TESTS = 10 ** 6
# random good tokens validate correctly
assert all(map(validate_token, [generate_token() for _ in range(N_OF_TESTS)]))
# can theoretically result in a false positive, but with the default values the chances for that are
# about as "good" as for winning a lottery
from random import choices
assert (any(map(validate_token, ["".join(choices("0123456789abcdef", k=TOKEN_LENGTH_IN_CHARS)) for _ in range(N_OF_TESTS)]))
is False
)