Ejemplo n.º 1
0
    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)
Ejemplo n.º 2
0
 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)
Ejemplo n.º 3
0
 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)
Ejemplo n.º 4
0
    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)
Ejemplo n.º 5
0
    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)
Ejemplo n.º 6
0
    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)
Ejemplo n.º 7
0
    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)
Ejemplo n.º 8
0
 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)
Ejemplo n.º 9
0
    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))
Ejemplo n.º 10
0
 def _check_events(self):
     eid = id or self.EventID
     data = yield self.api.events(eid, blocking=False)
     self.EventID = data['EventID']
     return_value(data)
Ejemplo n.º 11
0
    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)
Ejemplo n.º 12
0
 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)
Ejemplo n.º 13
0
 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)