def __init__(self, username: str, trust_keybase: bool=False, autofetch: bool=True) -> None: """ Create a new instance of a Keybase user. Params: - Username: The username to look up. This can be either a normal string username (`max`), or a username with a method (`github://maxtaco`). - trust_keybase: Should we pretend that keybase is correct, or do we locally verify the signatures? By default, we locally verify the signatures. - autofetch: Should we download the keybase data automagically, or should we not and just let the user replace the data? By default, the data is automatically fetched. """ self.username = username if "://" in username: self.method = username.split("://")[0] self.username = username.split("://")[-1] else: self.method = "usernames" self.fetched = False self.raw_keybase_data = None self.trust = trust_keybase if trust_keybase: warn("Trusting Keybase servers for this API request...") self.valid = False # Structure definitions. self.raw_public_key = None self.public_key = None self.fingerprint = "" self.keyalgo = 1 self.keybits = 0 self.proofs = ConfigKey() self.fullname = "" self.location = "" self.bio = "" # Fetch the data. if autofetch: self._get_info()
def _translate_into_configkey(self, data: requests.Response) -> ConfigKey: """ Transforms data into a ConfigKey object. """ if isinstance(data, requests.Response): if "application/json" in data.headers['Content-Type']: c = ConfigKey(); c.load_from_dict(data.json()); return c else: return None elif isinstance(data, dict): c = ConfigKey(); c.load_from_dict(data); return c else: return None
class User(_Keybase): """ A class for getting information about keybase Users. This supports things such as twitter://user or github://user. Note that if the search returns multiple results, the first one will be picked. """ def encrypt_data(self, message: str) -> pgp.message.EncryptedMessageWrapper: raise NotImplementedError("python-pgp does not currently support encrypting.") def verify_data(self, pgp_message: str) -> bool: # Just pass a call to _verify_msg return self._verify_msg(pgp_message) def __init__(self, username: str, trust_keybase: bool=False, autofetch: bool=True) -> None: """ Create a new instance of a Keybase user. Params: - Username: The username to look up. This can be either a normal string username (`max`), or a username with a method (`github://maxtaco`). - trust_keybase: Should we pretend that keybase is correct, or do we locally verify the signatures? By default, we locally verify the signatures. - autofetch: Should we download the keybase data automagically, or should we not and just let the user replace the data? By default, the data is automatically fetched. """ self.username = username if "://" in username: self.method = username.split("://")[0] self.username = username.split("://")[-1] else: self.method = "usernames" self.fetched = False self.raw_keybase_data = None self.trust = trust_keybase if trust_keybase: warn("Trusting Keybase servers for this API request...") self.valid = False # Structure definitions. self.raw_public_key = None self.public_key = None self.fingerprint = "" self.keyalgo = 1 self.keybits = 0 self.proofs = ConfigKey() self.fullname = "" self.location = "" self.bio = "" # Fetch the data. if autofetch: self._get_info() def _get_info(self): # Fetch the information from keybase. discovery = self._get("user/lookup.json", {self.method: self.username}) self.raw_keybase_data = discovery self._map_data() def _map_data(self): # Begin mapping data to our structure. if self.raw_keybase_data.status.code != 0: self.fetched = False return else: self.fetched = True # Load first person's profile data. if len(self.raw_keybase_data.them) <= 0: raise UserNotFoundError(self.method + "://" + self.username) person = self.raw_keybase_data.them[0] # Load basic data. self.real_name = person.profile.full_name self.location = person.profile.location self.bio = person.profile.bio self.username = person.basics.username # Map public key self.raw_public_key = person.public_keys.primary.bundle self.public_key = pgp.read_key(self.raw_public_key) self.fingerprint = person.public_keys.primary.key_fingerprint.upper() self.keyalgo = person.public_keys.primary.key_algo self.keybits = person.public_keys.primary.key_bits self.subkeys = set(key[-16:] for key in person.public_keys.sibkeys) # Loop over our proofs. for proof in person.proofs_summary.all: self.proofs[proof.proof_id] = ConfigKey() self.proofs[proof.proof_id].load_from_dict(proof) self.valid = True def _verify_msg(self, msg: str) -> bool: # Load in the message. try: loaded_msg = pgp.read_message(msg, armored=True) except ValueError as e: raise VerificationError("Message was invalid") from e # Verify the key. # First, find a signature that matches the Key ID. for sig in loaded_msg.get_message().signatures: if self.fingerprint[-16:] in sig.issuer_key_ids: # Verified! signature = sig key_to_use = self.public_key break else: # Check for subkeys. for subkey in self.public_key.subkeys: if subkey.fingerprint[-16:] in sig.issuer_key_ids: # Verified as well. key_to_use = subkey signature = sig break # This is a quick hack, to break out of both loops. else: continue break else: raise VerificationError("Could not find a valid self signature in proof") # Then, verify using the public key on store. return key_to_use.verify(signature, loaded_msg.get_message().message) def _find_pgp_data(self, data: str) -> str: d = data[data.find("-----BEGIN PGP MESSAGE-----"):data.find("-----END PGP MESSAGE-----")+26] # Strip HTML tags for dns sigs and the like. d = d.replace("<span class=\"hljs-horizontal_rule\">", "") d = d.replace("<span>", "") d = d.replace("</span>", "") d = d.replace("\"", "") d = d.replace("<", "").replace(">", "") return d def verify_proofs(self) -> bool: """ Verify the proofs of this user. This scans the available proof locations to find the PGP messages stored within, then verifies the signatures. Returns: True if verification succeeded. False if there was no proofs to verify. Raises: VerificationError if the proofs failed to verify/validate. """ if len(self.proofs.items()) == 0: return False if self.trust: warn("Blindly trusting Keybase servers that the proofs are valid...") for proof in self.proofs.values(): if proof.state == 1: continue else: raise VerificationError("Proof {} could not be verified!".format(proof.proof_type + "/" + proof.nametag)) return True # Otherwise... for proof in self.proofs.values(): # Get our URL. if proof.proof_type == "github": # Decode link. gist_id = proof.proof_url.split("/")[-1] request_url = "https://gist.githubusercontent.com/{}/{}/raw".format(proof.nametag, gist_id) r = requests.get(request_url, headers=headers) if r.status_code != 200: raise VerificationError("Proof URL could not be validated") else: # Search for the PGP key header... data = r.text key = self._find_pgp_data(data) if not self._verify_msg(key): raise VerificationError("Proof {} could not be verified!".format(proof.proof_type + "/" + proof.nametag)) elif proof.proof_type == "reddit": # Sigh. # Reddit's API is shittastic, and I don't have enough justification to use praw for just fetching these. r = requests.get(proof.proof_url + "/.json", headers=headers) js = r.json() # Get the parent user's data. to_search_mtree = js[0]["data"]["children"][0]["data"] # Verify the username. if to_search_mtree["author"].lower() != proof.nametag.lower(): raise VerificationError("Proof {} username does not match") data = self._find_pgp_data(to_search_mtree["selftext"]) # Next, strip the spaces from the left. ndata = [] for line in data.split('\n'): ndata.append(line.lstrip(' ')) ndata = '\n'.join(ndata) if not self._verify_msg(ndata): raise VerificationError("Proof {} could not be verified!".format(proof.proof_type + "/" + proof.nametag)) elif proof.proof_type in ["generic_web_site", "dns", "coinbase"]: # Nothing special here, just find the data in the URL, and verify it. data = requests.get(proof.proof_url, headers=headers) data = self._find_pgp_data(data.text) if not self._verify_msg(data): raise VerificationError("Proof {} could not be verified!".format(proof.proof_type + "/" + proof.nametag)) else: warn("Cannot verify proofs of type {} current due to lack of keybase API support, without HTML scraping.".format(proof.proof_type)) return True