def _read_message(self, message): if not isinstance(message, Message): raise TypeError("expected a protonmail.models.Message instance") # If the message hasn't been read yet, do that now if not message.Body: resp = yield self.api.messages(message.ID, blocking=False, response=responses.MessageResponse) if resp.Code != 1000: raise ValueError("Unexpected response: {}".format( resp.to_json())) message = resp.Message # Read and decrypt if needed msg = PGPMessage.from_blob(message.Body) if msg.is_signed: email = message.SenderAddress if email not in self.PublicKeys: yield self._get_public_key(email) pk = self.PublicKeys.get(email) if not pk: raise SecurityError("Failed to verify signed message!") pk[0].verify(msg) # TODO: Support mutiple keys # Decrypt with self.PrivateKey.unlock(self.MailboxPassword) as key: message.decrypt(key) return_value(message)
def _get_public_key(self, email): email = utils.str(email) r = yield self.api.keys('?Email={}'.format(email), blocking=False, response=responses.KeysResponse) self.PublicKeys[email] = [ PGPKey.from_blob(k.PublicKey)[0] for k in r.Keys ] return_value(r)
def _logout(self): client = self.client data = yield self.request("api/auth", method='DELETE', blocking=False) result = responses.Response.from_json(**data) if result.Code != 1000: raise LoginError("Unexpected logout code: {}".format(result.Code)) del client.AuthInfo del client.Auth del client.AuthCookies del client.MailboxPassword return_value(result)
def _create_draft(self, address=None, message=None): if message is None: # Create a new message user = self.User if not user: r = yield self._get_user_info() user = self.User if not address: addresses = self.Addresses if not addresses: r = yield self._get_addresses() addresses = self.Addresses if not addresses: raise ValueError("No email addresses available") address = addresses[0] name = (address.DisplayName or address.Name or user.DisplayName or user.Name) message = Message(AddressID=address.ID, IsRead=1, MIMEType='text/html', Sender=EmailAddress(Name=name, Address=address.Email)) # Make sure it's encrypted message.encrypt(self.PrivateKey.pubkey) r = yield self.api.messages(method='POST', blocking=False, response=responses.MessageResponse, json={ 'AttachmentKeyPackets': [], 'id': None, 'Message': message.to_json( 'AddressID', 'Sender', 'IsRead', 'CCList', 'BCCList', 'MIMEType', 'Subject', 'Body', 'ToList', ) }) if r.Message: r.Message.Client = self return_value(r)
def _unlock(self, password): client = self.client host = self.host headers = self.headers.copy() # Compute access key pwd = auth.compute_key_password(password, b64d(client.Auth.KeySalt)) # Decode the access token token = auth.check_mailbox_password(client.Auth.EncPrivateKey, pwd, client.Auth.AccessToken) # Stupid python 3 authorization = 'Bearer ' + utils.str(token) headers.update({ 'Accept': '*/*', 'Authorization': authorization, 'Referer': host + "login/unlock", 'x-pm-uid': client.Auth.Uid }) # Get the cookies r = yield requests.post(url=host + "api/auth/cookies", json=client.to_json( 'ResponseType', 'ClientID', 'GrantType', 'RedirectURI', 'State', Uid=client.Auth.Uid, RefreshToken=client.Auth.RefreshToken), headers=headers) if r.code != 200: raise LoginError("Unexpected unlock response: " "{}".format(r.code)) data = yield r.json() # Save the hashed mailbox password client.MailboxPassword = pwd result = responses.AuthCookiesResponse.from_json(**data) if result.Code == 1000: client.AuthCookies = result client.Cookies = r.cookies() return_value(result)
def _request(self, path, body=None, method='GET', cookies=None, headers=None, response=None, **kwargs): client = self.client if not client.Auth: raise LoginError("Must login first!") if not client.AuthCookies: raise AuthError("Must unlock the keys first!") if response is not None and not issubclass(response, responses.Response): raise ValueError("response must be a subclass Response. " "Got {}".format(response)) h = self.headers.copy() h.update({'x-pm-uid': client.Auth.Uid}) url = self.host + path.lstrip("/") if client.debug: log.warning("Request: method={}, url={}, body={}, cookies={}, " "headers={} kwargs={}".format(method, url, body, cookies, headers, pformat(kwargs))) r = yield requests.request(method=method, url=url, body=body, cookies=cookies or client.Cookies, headers=headers or h, **kwargs) if r.code != 200: log.warning("Unexpected HTTP response: {}".format(r.code)) data = yield r.json() if client.debug: log.warning("Response: {} - {}".format(r, pformat(data))) if response is not None: return_value(response.from_json(**data)) return_value(data)
def _save_draft(self, message): if not isinstance(message, Message): raise TypeError("expected a protonmail.models.Message instance") if not message.ID: raise ValueError("Cannot save a draft without an ID. " "Use create_draft first.") # Encrypt for this client only message.encrypt(self.PrivateKey.pubkey) # Should never happen if not message.is_encrypted(): raise SecurityError("Failed to encrypted draft") r = yield self.api.messages(message.ID, method='PUT', blocking=False, response=responses.MessageResponse, json={ 'AttachmentKeyPackets': {}, 'id': message.ID, 'Message': message.to_json( 'AddressID', 'Sender', 'IsRead', 'CCList', 'BCCList', 'MIMEType', 'Subject', 'Body', 'ToList', ) }) if r.Message: r.Message.Client = self return_value(r)
def _send_simple(self, to, subject="", body="", cc=None, bcc=None): if not to: raise ValueError("Please enter one or more recipient email " "addresses") r = yield self._create_draft() if r.Code != 1000: raise ValueError("Failed to create draft: {}".format(r.to_json())) m = r.Message m.Subject = subject m.DecryptedBody = body if not isinstance(to, (tuple, list)): to = [to] m.ToList = [EmailAddress(Address=addr) for addr in to] if cc is not None: m.CCList = [EmailAddress(Address=addr) for addr in cc] if bcc is not None: m.BCCList = [EmailAddress(Address=addr) for addr in bcc] r = yield self._save_draft(m) if r.Code != 1000: raise ValueError("Failed to save draft: {}".format(r.to_json())) r = yield self._send_message(m) if r.Code != 1000: raise ValueError("Failed to send message: {}".format(r.to_json())) return_value(r)
def _login(self, password, unlock=True): client = self.client host = self.host headers = self.headers.copy() headers.update({ 'Accept': 'application/vnd.protonmail.v1+json', 'Referer': host + "login", }) # Get the auth info r = yield requests.post(url=host + "api/auth/info", json=client.to_json('Username', 'ClientID'), headers=headers) if r.code != 200: try: data = yield r.json() except: data = {} raise LoginError("Unexpected info response: " "{} - {}".format(r.code, data)) data = yield r.json() # Parses the response and computes the proof resp = client.AuthInfo = responses.AuthInfoResponse.from_json(**data) if resp.Code != 1000: raise LoginError("Unexpected info code: {}".format(resp.Code)) if resp.TwoFactor: raise NotImplementedError("Two factor auth is not implemented") # Compute the hashed password client.HashedPassword = auth.hash_password( int(client.ApiVersion), password, b64d(resp.Salt), client.Username, b64d(auth.read_armored(resp.Modulus))) # Update headers headers.update({ 'Accept': '*/*', }) # Authenticate r = yield requests.post( url=host + "api/auth", json=client.to_json('Username', 'ClientID', 'ClientSecret', 'TwoFactorCode', SRPSession=client.AuthInfo.SRPSession, ClientProof=b64e(client.ClientProof), ClientEphemeral=b64e(client.ClientEphemeral)), headers=headers) if r.code != 200: try: data = yield r.json() except: data = {} raise LoginError("Unexpected auth response: " "{} - {}".format(r.code, data)) data = yield r.json() resp = client.Auth = responses.AuthResponse.from_json(**data) if resp.Code != 1000: del client.Auth raise LoginError("Unexpected auth code: {}".format(resp.Code)) if b64d(resp.ServerProof) != client.ExpectedServerProof: del client.Auth raise AuthError("Invalid server authentication") # Login success if not unlock: return_value(resp) # And unlock unlocked = yield self._unlock(password) return_value((resp, unlocked))
def _check_events(self): eid = id or self.EventID data = yield self.api.events(eid, blocking=False) self.EventID = data['EventID'] return_value(data)
def _send_message(self, message): if not isinstance(message, Message): raise TypeError("expected a protonmail.models.Message instance") if not message.ToList: raise ValueError("message missing email to addresses") # Read draft from server if needed if message.ID and not message.Body: r = yield self.api.messages(message.ID, blocking=False, response=responses.MessageResponse) message = r.Message # Decrypt if message.Body and not message.DecryptedBody: yield self._read_message(message) # Get any missing keys keys = self.PublicKeys emails = list( set([ to.Address for to in (message.ToList + message.CCList + message.BCCList) ])) for e in emails: if e not in keys: yield self._get_public_key(e) keys = self.PublicKeys # Extract the session key #cipher = SymmetricKeyAlgorithm.AES256 #session_key = auth.generate_session_key(cipher) with self.PrivateKey.unlock(self.MailboxPassword) as uk: cipher, session_key = auth.decrypt_session_key(message.Body, key=uk) pkg = { 'Addresses': {}, 'Body': "", 'MIMEType': message.MIMEType or "text/html", 'Type': 0, } # If we need to send the key in clear cleartext = False for to in message.ToList: pk = keys.get(to.Address) if pk is None: raise SecurityError("Failed to get public key for: " "{}".format(to.Address)) if pk: # Inside user # I guess the server does this? Encrypt body for email's pubkey #pkg['Body'] = pk.encrypt(pkg['Body'], cipher=cipher, # sessionkey=session_key) # Encrypt the session key for this user # TODO: Support multiple keys sk = auth.encrypt_session_key(session_key, key=pk[0], cipher=cipher) pkg['Addresses'][to.Address] = { 'AttachmentKeyPackets': {}, 'BodyKeyPacket': utils.str(b64e(sk)), 'Signature': 0, 'Type': Message.SEND_PM } pkg['Type'] |= Message.SEND_PM elif False and message.IsEncrypted: # Disabled for now # Enc outside user token = message.generate_reply_token(cipher) enc_token = PGPMessage.new(b64d(token)).encrypt( message.Password).message.__bytes__() pkg['Addresses'][to.Address] = { 'Auth': 0, 'PasswordHint': message.PasswordHint, 'Type': Message.SEND_EO, 'Token': token, 'EncToken': utils.str(b64e(enc_token)), 'AttachmentKeyPackets': {}, 'BodyKeyPacket': utils.str(b64e(session_key)), 'Signature': int(pkg['Body'].is_signed), } else: cleartext = True # Outside user pkg['Addresses'][to.Address] = { 'Signature': 0, 'Type': Message.SEND_CLEAR } pkg['Type'] |= Message.SEND_CLEAR if cleartext and message.ExpirationTime and not message.Password: raise SecurityError("Expiring emails to non-ProtonMail recipients" \ "require a message password to be set") # Sending to a non PM user screws all security if cleartext: pkg['BodyKey'] = { 'Algorithm': cipher.name.lower(), 'Key': utils.str(b64e(session_key)) } pkg['AttachmentKeys'] = {} # TODO # Get the message msg = PGPMessage.new(message.DecryptedBody) # Sign it with self.PrivateKey.unlock(self.MailboxPassword) as uk: msg |= uk.sign(msg) # Encrypt it using the session key and encode it msg = self.PrivateKey.pubkey.encrypt(msg, cipher=cipher, sessionkey=session_key) # Now encode it pkg['Body'] = utils.str(b64e(msg.message.__bytes__())) r = yield self.api.messages(message.ID, method='POST', blocking=False, response=responses.MessageSendResponse, json={ 'ExpirationTime': 0, 'id': message.ID, 'Packages': [pkg] }) return_value(r)
def _get_user_info(self): r = yield self.api.users(blocking=False, response=responses.UsersResponse) if r.Code == 1000: self.User = r.User return_value(r)
def _get_addresses(self): r = yield self.api.addresses(blocking=False, response=responses.AddressesResponse) if r.Code == 1000: self.Addresses = r.Addresses return_value(r)