mirror of
https://github.com/lnbits/lnbits-legend.git
synced 2024-11-20 10:39:59 +01:00
110 lines
3.4 KiB
Python
110 lines
3.4 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
|
|
import getpass
|
|
|
|
|
|
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
|
|
if macaroon.split(".")[-1] == "macaroon":
|
|
with open(macaroon, "rb") as f:
|
|
macaroon_bytes = f.read()
|
|
return macaroon_bytes.hex()
|
|
else:
|
|
# convert the bas64 macaroon to hex
|
|
try:
|
|
macaroon = base64.b64decode(macaroon).hex()
|
|
except:
|
|
pass
|
|
return macaroon
|
|
|
|
|
|
class AESCipher(object):
|
|
"""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 type(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)
|