father-bot / chatgpt_telegram_bot

💬 Telegram bot with ChatGPT, Python-based, using OpenAI's API.
https://t.me/chatgpt_karfly_bot
MIT License
5.14k stars 1.8k forks source link

Allow users to upload their own openAI API key and pay the bill themself #97

Open EasonC13 opened 1 year ago

EasonC13 commented 1 year ago

Hi, this repo is awesome!

Curious if the team is interested in including a version that requires users to upload their OpenAI API key first to use ChatGPT. Maybe the host could enable this feature by setting a boolean in config.

By doing so, the host can open the bot to others who don't have the resource to host but want to use ChatGPT and pay the bill by themself.

If yes, I can contribute to that part.

Regarding the security issue, we could use encryption methods to encrypt the key users uploaded by their userID and username. Hence, the host can't access their key in Database directly. Maybe need to hash the user id and username to create a mapping to de-identify users in the database as well.

Thank you.

karfly commented 1 year ago

Hi, I don't think it's a good feature because of security reasons. Is it fundamentally possible to transfer the secret key so that the admin is not able to find out what the key is?

EasonC13 commented 1 year ago

Hi @karfly

Yes, we can do it by cryptography.

Please check the following proof of concept code.

We can get the master key when receiving an API call while keeping it unidentified and unobtainable without the telegram API call from the user.

Users will need to set their API key again when they change chat information such as first name, last name, or username.

The hacker can't get the master key without the chat information and the bot's private key.

Moreover, we do need to warn users about the possibility that their keys will get hacked and recommend they set Usage Limits at https://platform.openai.com/account/billing/limits.

import hashlib
from base64 import urlsafe_b64encode
from cryptography.fernet import Fernet
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
from cryptography.hazmat.backends import default_backend

import pymongo
db = pymongo.database.Database
col = db["users_openAI_keys"]

def Sha512Hash(text):
    hashed = hashlib.sha512(text.encode("utf-8")).hexdigest()
    return hashed

def encrypt_text(key, text: bytes):
    fernet = Fernet(key)
    return fernet.encrypt(text)

def decrypt_text(key, encrypted_text: bytes):
    fernet = Fernet(key)
    return fernet.decrypt(encrypted_text)

def generate_fernet_key(seed: bytes):
    # Use the same salt for key recovery.
    salt = b'*)X\x1bz"z\xd0\x8a\xdc\x91\xef*\x9c\x00\x0b'
    kdf = PBKDF2HMAC(
        algorithm=hashes.SHA512(),
        length=32,
        salt=salt,
        iterations=100,
        backend=default_backend(),
    )
    return urlsafe_b64encode(kdf.derive(seed))

def get_users_master_key(
    update: telegram.update.Update, context: callbackcontext.CallbackContext
):
    """
    We can get the master key when receiving an API call 
    while keeping it unidentified and unobtainable 
    without the telegram API call from the user. 

    Users will need to set their API key again 
    when they change chat information 
    such as first name, last name, or username.

    The hacker can't get the master key 
    without the chat information and the bot's private key.
    """

    master_key = Sha512Hash(f"{context.bot.private_key}_{str(update.message.chat)}")
    return master_key

def set_users_openAI_key(
    openAI_API_key: str,
    update: telegram.update.Update,
    context: callbackcontext.CallbackContext,
):
    master_key = get_users_master_key(update, context)
    fernet_key = generate_fernet_key(master_key.encode())
    openAI_API_key_encrypted = encrypt_text(fernet_key, openAI_API_key.encode())
    col.insert_one(
        {
            "key": Sha512Hash(master_key),  # double hash as index
            "openAI_API_key": openAI_API_key_encrypted,
        }
    )

def get_users_openAI_key(
    update: telegram.update.Update,
    context: callbackcontext.CallbackContext,
):
    master_key = get_users_master_key(update, context)
    result = col.find_one(
        {
            "key": Sha512Hash(master_key),  # double hash as index
        }
    )
    fernet_key = generate_fernet_key(master_key.encode())
    openAI_API_key = decrypt_text(fernet_key, result["openAI_API_key"])
    return openAI_API_key.decode()
EasonC13 commented 1 year ago

Hi Want to know the follow-up about this proof-of-concept code.

What do you guys think?

karfly commented 1 year ago

I'll answer later, sorry. Right now I'm fully focused on message streaming and DALL-E 2 support