lnbits-legend/lnbits/wallets/macaroon/macaroon.py
Vlad Stan c9093715b7
[FEAT] Auth, Login, OAuth, create account with username and password #1653 (#2092)
no more superuser url!
delete cookie on logout
add usr login feature
fix node management
* Cleaned up login form
* CreateUser
* information leak
* cleaner parsing usr from url
* rename decorators
* login secret
* fix: add back `superuser` command
* chore: remove `fastapi_login`
* fix: extract `token` from cookie
* chore: prepare to extract user
* feat: check user
* chore: code clean-up
* feat: happy flow working
* fix: usr only login
* fix: user already logged in
* feat: check user in URL
* fix: verify password at DB level
* fix: do not show `Login` controls if user already logged in
* fix: separate login endpoints
* fix: remove `usr` param
* chore: update error message
* refactor: register method
* feat: logout
* chore: move comments
* fix: remove user auth check from API
* fix: user check unnecessary
* fix: redirect after logout
* chore: remove garbage files
* refactor: simplify constructor call
* fix: hide user icon if not authorized
* refactor: rename auth env vars
* chore: code clean-up
* fix: add types for `python-jose`
* fix: add types for `passlib`
* fix: return type
* feat: set default value for `auth_secret_key` to hash of super user
* fix: default value
* feat: rework login page
* feat: ui polishing
* feat: google auth
* feat: add google auth
* chore: remove `authlib` dependency
* refactor: extract `_handle_sso_login` method
* refactor: convert methods to `properties`
* refactor: rename: `user_api` to `auth_api`
* feat: store user info from SSO
* chore: re-arange the buttons
* feat: conditional rendering of login options
* feat: correctly render buttons
* fix: re-add `Claim Bitcoin` from the main page
* fix: create wallet must send new user
* fix:  no `username-password` auth method
* refactor: rename auth method
* fix: do not force API level UUID4 validation
* feat: add validation for username
* feat: add account page
* feat: update account
* feat: add `has_password` for user
* fix: email not editable
* feat: validate email for existing account
* fix: register check
* feat: reset password
* chore: code clean-up
* feat: handle token expired
* fix: only redirect if `text/html`
* refactor: remove `OAuth2PasswordRequestForm`
* chore: remove `python-multipart` dependency
* fix: handle no headers for exception
* feat: add back button on error screen
* feat: show user profile image
* fix: check account creation permissions
* fix: auth for internal api call
* chore: add some docs
* chore: code clean-up
* fix: rebase stuff
* fix: default value types
* refactor: customize error messages
* fix: move types libs to dev dependencies
* doc: specify the `Authorization callback URL`
* fix: pass missing superuser id in node ui test
* fix: keep usr param on wallet redirect
removing usr param causes an issue if the browser doesnt yet have an access token.
* fix: do not redirect if `wal` query param not present
* fix: add nativeBuildInputs and buildInputs overrides to flake.nix
* bump fastapi-sso to 0.9.0 which fixes some security issues
* refactor: move the `lnbits_admin_extensions` to decorators
* chore: bring package config from `dev`
* chore: re-add dependencies
* chore: re-add cev dependencies
* chore: re-add mypy ignores
* feat: i18n
* refactor: move admin ext check to decorator (fix after rebase)
* fix: label mapping
* fix: re-fetch user after first wallet was created
* fix: unlikely case that `user` is not found
* refactor translations (move '*' to code)
* reorganize deps in pyproject.toml, add comment
* update flake.lock and simplify flake.nix after upstreaming
overrides for fastapi-sso, types-passlib, types-pyasn1, types-python-jose
were upstreamed in https://github.com/nix-community/poetry2nix/pull/1463
* fix: more relaxed email verification (by @prusnak)
* fix: remove `\b` (boundaries) since we re using `fullmatch`
* chore: `make bundle`

---------

Co-authored-by: dni  <office@dnilabs.com>
Co-authored-by: Arc <ben@arc.wales>
Co-authored-by: jackstar12 <jkranawetter05@gmail.com>
Co-authored-by: Pavol Rusnak <pavol@rusnak.io>
2023-12-12 11:38:19 +01:00

116 lines
3.7 KiB
Python

import base64
import getpass
from hashlib import md5
from Cryptodome import Random
from Cryptodome.Cipher import AES
from loguru import logger
BLOCK_SIZE = 16
def load_macaroon(macaroon: str) -> str:
"""Returns hex version of a macaroon encoded in base64 or the file path.
:param macaroon: Macaroon encoded in base64 or file path.
:type macaroon: str
:return: Hex version of macaroon.
:rtype: str
"""
# if the macaroon is a file path, load it and return hex version
if macaroon.split(".")[-1] == "macaroon":
with open(macaroon, "rb") as f:
macaroon_bytes = f.read()
return macaroon_bytes.hex()
else:
# if macaroon is a provided string
# check if it is hex, if so, return
try:
bytes.fromhex(macaroon)
return macaroon
except ValueError:
pass
# convert the bas64 macaroon to hex
try:
macaroon = base64.b64decode(macaroon).hex()
except Exception:
pass
return macaroon
# todo: move to its own (crypto.py) file
class AESCipher:
"""This class is compatible with crypto-js/aes.js
Encrypt and decrypt in Javascript using:
import AES from "crypto-js/aes.js";
import Utf8 from "crypto-js/enc-utf8.js";
AES.encrypt(decrypted, password).toString()
AES.decrypt(encrypted, password).toString(Utf8);
"""
def __init__(self, key=None, description=""):
self.key = key
self.description = description + " "
def pad(self, data):
length = BLOCK_SIZE - (len(data) % BLOCK_SIZE)
return data + (chr(length) * length).encode()
def unpad(self, data):
return data[: -(data[-1] if isinstance(data[-1], int) else ord(data[-1]))]
@property
def passphrase(self):
passphrase = self.key if self.key is not None else None
if passphrase is None:
passphrase = getpass.getpass(f"Enter {self.description}password:")
return passphrase
def bytes_to_key(self, data, salt, output=48):
# extended from https://gist.github.com/gsakkis/4546068
assert len(salt) == 8, len(salt)
data += salt
key = md5(data).digest()
final_key = key
while len(final_key) < output:
key = md5(key + data).digest()
final_key += key
return final_key[:output]
def decrypt(self, encrypted: str) -> str: # type: ignore
"""Decrypts a string using AES-256-CBC."""
passphrase = self.passphrase
encrypted = base64.b64decode(encrypted) # type: ignore
assert encrypted[0:8] == b"Salted__"
salt = encrypted[8:16]
key_iv = self.bytes_to_key(passphrase.encode(), salt, 32 + 16)
key = key_iv[:32]
iv = key_iv[32:]
aes = AES.new(key, AES.MODE_CBC, iv)
try:
return self.unpad(aes.decrypt(encrypted[16:])).decode() # type: ignore
except UnicodeDecodeError:
raise ValueError("Wrong passphrase")
def encrypt(self, message: bytes) -> str:
passphrase = self.passphrase
salt = Random.new().read(8)
key_iv = self.bytes_to_key(passphrase.encode(), salt, 32 + 16)
key = key_iv[:32]
iv = key_iv[32:]
aes = AES.new(key, AES.MODE_CBC, iv)
return base64.b64encode(
b"Salted__" + salt + aes.encrypt(self.pad(message))
).decode()
# if this file is executed directly, ask for a macaroon and encrypt it
if __name__ == "__main__":
macaroon = input("Enter macaroon: ")
macaroon = load_macaroon(macaroon)
macaroon = AESCipher(description="encryption").encrypt(macaroon.encode())
logger.info("Encrypted macaroon:")
logger.info(macaroon)