diff --git a/Dockerfile b/Dockerfile index ac8b912..332c34e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,7 +5,9 @@ RUN apt-get update && \ apache2 apache2-dev \ python3-dev \ default-libmysqlclient-dev \ - supervisor && \ + supervisor \ + libsqlcipher-dev \ + libsqlite3-dev && \ rm -rf /var/lib/apt/lists/* WORKDIR /smswithoutborders-backend diff --git a/docs/grpc.md b/docs/grpc.md index 2f81361..d97ac38 100644 --- a/docs/grpc.md +++ b/docs/grpc.md @@ -14,7 +14,8 @@ - [Complete Authentication](#complete-authentication) - [List an Entity's Stored Tokens](#list-an-entitys-stored-tokens) - [Store an Entity's Token](#store-an-entitys-token) - - [Get Entity Access Token and Decrypt Payload](#get-entity-access-token-and-decrypt-payload) + - [Get Entity Access Token](#get-entity-access-token) + - [Decrypt Payload](#decrypt-payload) - [Encrypt Payload](#encrypt-payload) - [Update An Entity Token](#update-an-entitys-token) @@ -607,13 +608,13 @@ localhost:6000 vault.v1.Entity/StoreEntityToken `request` **GetEntityAccessTokenAndDecryptPayloadRequest** +> `request` **GetEntityAccessTokenRequest** > [!IMPORTANT] > @@ -623,28 +624,27 @@ This function retrieves an entity's access token and decrypts the payload. | Field | Type | Description | | ------------------ | ------ | ------------------------------------------------------------------- | | device_id | string | The unique identifier of the device used by the entity. | -| payload_ciphertext | string | The encrypted payload ciphertext that needs to be decrypted. | | platform | string | The platform from which the token is being issued. (e.g., "gmail"). | +| account_identifier | string | The identifier of the account associated with the token. | --- -> `response` **GetEntityAccessTokenAndDecryptPayloadResponse** +> `response` **GetEntityAccessTokenResponse** > [!IMPORTANT] > > The table lists only the fields that are populated for this step. Other fields > may be empty, omitted, or false. -| Field | Type | Description | -| ----------------- | ------ | -------------------------------------------------------------------------- | -| message | string | A response message from the server. | -| success | bool | Indicates if the operation was successful. | -| payload_plaintext | string | The decrypted payload plaintext. | -| token | string | The retrieved token associated with the entity for the specified platform. | +| Field | Type | Description | +| ------- | ------ | -------------------------------------------------------------------------- | +| message | string | A response message from the server. | +| success | bool | Indicates if the operation was successful. | +| token | string | The retrieved token associated with the entity for the specified platform. | --- -> `method` **GetEntityAccessTokenAndDecryptPayload** +> `method` **GetEntityAccessToken** > [!TIP] > @@ -666,9 +666,9 @@ This function retrieves an entity's access token and decrypts the payload. ```bash grpcurl -plaintext \ - -d '{"device_id": "device_id", "payload_ciphertext": "encrypted_payload", "platform": "gmail"}' \ + -d '{"device_id": "device_id", "platform": "gmail", "account_identifier": "sample@mail.com"}' \ -proto protos/v1/vault.proto \ -localhost:6000 vault.v1.Entity/GetEntityAccessTokenAndDecryptPayload +localhost:6000 vault.v1.Entity/GetEntityAccessToken ``` --- @@ -677,15 +677,90 @@ localhost:6000 vault.v1.Entity/GetEntityAccessTokenAndDecryptPayload ```json { - "message": "Successfully fetched tokens and decrypted payload", + "message": "Successfully fetched tokens", "success": true, - "payload_plaintext": "Decrypted payload content", "token": "retrieved_token" } ``` --- +#### Decrypt Payload + +This function handles decrypting payload content. + +--- + +> `request` **DecryptPayloadRequest** + +> [!IMPORTANT] +> +> The table lists only the required fields for this step. Other fields will be +> ignored. + +| Field | Type | Description | +| ------------------ | ------ | ------------------------------------------------------------ | +| device_id | string | The unique identifier of the device used by the entity. | +| payload_ciphertext | string | The encrypted payload ciphertext that needs to be decrypted. | + +--- + +> `response` **DecryptPayloadResponse** + +> [!IMPORTANT] +> +> The table lists only the fields that are populated for this step. Other fields +> may be empty, omitted, or false. + +| Field | Type | Description | +| ----------------- | ------ | ------------------------------------------ | +| message | string | A response message from the server. | +| success | bool | Indicates if the operation was successful. | +| payload_plaintext | string | The decrypted payload plaintext. | + +--- + +> `method` **DecryptPayload** + +> [!TIP] +> +> The examples below use +> [grpcurl](https://github.com/fullstorydev/grpcurl#grpcurl). + +> [!NOTE] +> +> Here is what a successful response from the server looks like. +> +> The server would return a status code of `0 OK` if the API transaction goes +> through without any friction. Otherwise, it will return any other code out of +> the +> [17 codes supported by gRPC](https://grpc.github.io/grpc/core/md_doc_statuscodes.html). + +--- + +**Sample request** + +```bash +grpcurl -plaintext \ + -d '{"device_id": "device_id", "payload_ciphertext": "encrypted_payload"}' \ + -proto protos/v1/vault.proto \ +localhost:6000 vault.v1.Entity/DecryptPayload +``` + +--- + +**Sample response** + +```json +{ + "message": "Successfully decrypted payload", + "success": true, + "payload_plaintext": "Decrypted payload content" +} +``` + +--- + #### Encrypt Payload This function handles the encryption of payload content. @@ -820,7 +895,7 @@ This function updates tokens associated with an entity. ```bash grpcurl -plaintext \ - -d '{"device_id": "device_id", "token": "new_token", "platform": "gmail", "account_identifier": "account_id"}' \ + -d '{"device_id": "device_id", "token": "new_token", "platform": "gmail", "account_identifier": "sample@mail.com"}' \ -proto protos/v1/vault.proto \ localhost:6000 vault.v1.Entity/UpdateEntityToken ``` diff --git a/protos/v1/vault.proto b/protos/v1/vault.proto index bbf5104..46e6e45 100644 --- a/protos/v1/vault.proto +++ b/protos/v1/vault.proto @@ -106,26 +106,41 @@ message StoreEntityTokenResponse { bool success = 2; } -// Request message for getting entity access token and decrypting payload. -message GetEntityAccessTokenAndDecryptPayloadRequest { +// Request message for getting entity access token. +message GetEntityAccessTokenRequest { // Device ID for identifying the requesting device. string device_id = 1; - // Encrypted payload that needs to be decrypted. - string payload_ciphertext = 2; // The platform associated with the token. - string platform = 3; + string platform = 2; + // The identifier of the account associated with the token. + string account_identifier = 3; } -// Response message for getting entity access token and decrypting payload. -message GetEntityAccessTokenAndDecryptPayloadResponse { +// Response message for getting entity access token. +message GetEntityAccessTokenResponse { // Entity access token (JSON string). string token = 1; + // A response message. + string message = 2; + // Indicates whether the operation was successful. + bool success = 3; +} + +// Request message for decrypting payload. +message DecryptPayloadRequest { + // Device ID for identifying the requesting device. + string device_id = 1; + // Encrypted payload that needs to be decrypted. + string payload_ciphertext = 2; +} + +message DecryptPayloadResponse { // Decrypted plaintext payload. - string payload_plaintext = 2; + string payload_plaintext = 1; // A response message. - string message = 3; + string message = 2; // Indicates whether the operation was successful. - bool success = 4; + bool success = 3; } // Request message for encrypting payload. @@ -176,8 +191,10 @@ service Entity { rpc ListEntityStoredTokens (ListEntityStoredTokenRequest) returns (ListEntityStoredTokenResponse); // Stores a token for an entity. rpc StoreEntityToken (StoreEntityTokenRequest) returns (StoreEntityTokenResponse); - // Get an entity's access token and decrypt payload. - rpc GetEntityAccessTokenAndDecryptPayload (GetEntityAccessTokenAndDecryptPayloadRequest) returns (GetEntityAccessTokenAndDecryptPayloadResponse); + // Get an entity's access token. + rpc GetEntityAccessToken (GetEntityAccessTokenRequest) returns (GetEntityAccessTokenResponse); + // Decrypt payload. + rpc DecryptPayload (DecryptPayloadRequest) returns (DecryptPayloadResponse); // Encrypt payload. rpc EncryptPayload (EncryptPayloadRequest) returns (EncryptPayloadResponse); // Updates a token for an entity. diff --git a/src/grpc_entity_service.py b/src/grpc_entity_service.py index 80fcc79..3fd2261 100644 --- a/src/grpc_entity_service.py +++ b/src/grpc_entity_service.py @@ -23,7 +23,6 @@ is_valid_x25519_public_key, decrypt_and_decode, load_keypair_object, - error_response, ) from src.long_lived_token import generate_llt, verify_llt from src.device_id import compute_device_id @@ -44,6 +43,35 @@ logger = logging.getLogger("[gRPC Entity Service]") +def error_response(context, response, sys_msg, status_code, user_msg=None, _type=None): + """ + Create an error response. + + Args: + context: gRPC context. + response: gRPC response object. + sys_msg (str or tuple): System message. + status_code: gRPC status code. + user_msg (str or tuple): User-friendly message. + _type (str): Type of error. + + Returns: + An instance of the specified response with the error set. + """ + if not user_msg: + user_msg = sys_msg + + if _type == "UNKNOWN": + logger.exception(sys_msg, exc_info=True) + else: + logger.error(sys_msg) + + context.set_details(user_msg) + context.set_code(status_code) + + return response() + + def validate_request_fields(context, request, response, required_fields): """ Validates the fields in the gRPC request. @@ -365,8 +393,8 @@ def initiate_authentication(request, entity_obj): f"Incorrect Password provided for phone number {request.phone_number}", grpc.StatusCode.UNAUTHENTICATED, user_msg=( - "Incorrect credentials. Please double-check ", - "your details and try again.", + "Incorrect credentials. Please double-check " + "your details and try again." ), ) @@ -378,6 +406,7 @@ def initiate_authentication(request, entity_obj): message, expires = pow_response entity_obj.device_id = None + entity_obj.server_state = None entity_obj.save() return response( @@ -589,7 +618,7 @@ def StoreEntityToken(self, request, context): logger.info("Successfully stored tokens for %s", entity_obj.eid) return response( - message=f"Token stored successfully.", + message="Token stored successfully.", success=True, ) @@ -611,73 +640,130 @@ def StoreEntityToken(self, request, context): _type="UNKNOWN", ) - def GetEntityAccessTokenAndDecryptPayload(self, request, context): - """Handles getting an entity's access token and decrypting payload""" + def DecryptPayload(self, request, context): + """Handles decrypting relaysms payload""" - response = vault_pb2.GetEntityAccessTokenAndDecryptPayloadResponse + response = vault_pb2.DecryptPayloadResponse - try: - invalid_fields_response = validate_request_fields( + def validate_fields(): + return validate_request_fields( context, request, response, - ["device_id", "payload_ciphertext", "platform"], + ["device_id", "payload_ciphertext"], ) - if invalid_fields_response: - return invalid_fields_response - entity_obj = find_entity(device_id=request.device_id) + def decode_message(): + header, content_ciphertext, decode_error = decode_relay_sms_payload( + request.payload_ciphertext + ) - if not entity_obj: - return error_response( + if decode_error: + return None, error_response( context, response, - f"Invalid device ID '{request.device_id}'. " - "Please log in again to obtain a valid device ID.", - grpc.StatusCode.UNAUTHENTICATED, + decode_error, + grpc.StatusCode.INVALID_ARGUMENT, + user_msg="Invalid content format.", + _type="UNKNOWN", ) - entity_publish_keypair = load_keypair_object(entity_obj.publish_keypair) - publish_shared_key = entity_publish_keypair.agree( + return (header, content_ciphertext), None + + def decrypt_message(entity_obj, header, content_ciphertext): + publish_keypair = load_keypair_object(entity_obj.publish_keypair) + publish_shared_key = publish_keypair.agree( base64.b64decode(entity_obj.client_publish_pub_key) ) - header, content_ciphertext, decode_error = decode_relay_sms_payload( - request.payload_ciphertext + content_plaintext, state, decrypt_error = decrypt_payload( + server_state=entity_obj.server_state, + publish_shared_key=publish_shared_key, + publish_keypair=publish_keypair, + ratchet_header=header, + encrypted_content=content_ciphertext, + publish_pub_key=publish_keypair.get_public_key(), ) - if decode_error: + if decrypt_error: return error_response( context, response, - decode_error, + decrypt_error, grpc.StatusCode.INVALID_ARGUMENT, user_msg="Invalid content format.", _type="UNKNOWN", ) - content_plaintext = content_ciphertext.decode("utf-8") - # content_plaintext, state, decrypt_error = decrypt_payload( - # server_state=entity_obj.server_state, - # publish_shared_key=publish_shared_key, - # keypair=load_keypair_object(entity_obj.publish_keypair), - # ratchet_header=header, - # encrypted_content=content_ciphertext, - # client_pub_key=base64.b64decode(entity_obj.client_publish_pub_key), - # ) + entity_obj.server_state = state.serialize() + entity_obj.save() + logger.info( + "Successfully decrypted payload for %s", + entity_obj.eid, + ) + + return response( + message="Successfully decrypted payload", + success=True, + payload_plaintext=content_plaintext, + ) + + try: + invalid_fields_response = validate_fields() + if invalid_fields_response: + return invalid_fields_response + + entity_obj = find_entity(device_id=request.device_id) + + if not entity_obj: + return error_response( + context, + response, + f"Invalid device ID '{request.device_id}'. " + "Please log in again to obtain a valid device ID.", + grpc.StatusCode.UNAUTHENTICATED, + ) + + decoded_response, decoding_error = decode_message() + if decoding_error: + return decoding_error + + header, content_ciphertext = decoded_response + + return decrypt_message(entity_obj, header, content_ciphertext) - # if decrypt_error: - # return error_response( - # context, - # response, - # decrypt_error, - # grpc.StatusCode.INVALID_ARGUMENT, - # user_msg="Invalid content format.", - # _type="UNKNOWN", - # ) + except Exception as e: + return error_response( + context, + response, + e, + grpc.StatusCode.INTERNAL, + user_msg="Oops! Something went wrong. Please try again later.", + _type="UNKNOWN", + ) + + def GetEntityAccessToken(self, request, context): + """Handles getting an entity's access token.""" + + response = vault_pb2.GetEntityAccessTokenResponse + + def validate_fields(): + return validate_request_fields( + context, + request, + response, + ["device_id", "platform", "account_identifier"], + ) + + try: + invalid_fields_response = validate_fields() + if invalid_fields_response: + return invalid_fields_response + + entity_obj = find_entity(device_id=request.device_id) account_identifier_hash = generate_hmac( - HASHING_KEY, content_plaintext.split(":")[0] + HASHING_KEY, request.account_identifier ) tokens = fetch_entity_tokens( entity=entity_obj, @@ -691,17 +777,23 @@ def GetEntityAccessTokenAndDecryptPayload(self, request, context): if field in token: token[field] = decrypt_and_decode(token[field]) - # entity_obj.server_state = state.serialize() - # entity_obj.save() logger.info( - "Successfully fetched tokens and decrypted payload for %s", + "Successfully fetched tokens for %s", entity_obj.eid, ) + if not tokens: + return error_response( + context, + response, + "No token found with account " + f"identifier {request.account_identifier} for {request.platform}", + grpc.StatusCode.NOT_FOUND, + ) + return response( - message="Successfully fetched tokens and decrypted payload", + message="Successfully fetched tokens.", success=True, - payload_plaintext=content_plaintext, token=tokens[0]["account_tokens"], ) diff --git a/src/relaysms_payload.py b/src/relaysms_payload.py index d8c767b..3beff87 100644 --- a/src/relaysms_payload.py +++ b/src/relaysms_payload.py @@ -13,7 +13,9 @@ logger = logging.getLogger(__name__) -def decrypt_payload(server_state, keypair, ratchet_header, encrypted_content, **kwargs): +def decrypt_payload( + server_state, publish_keypair, ratchet_header, encrypted_content, **kwargs +): """ Decrypts a RelaySMS payload. @@ -33,32 +35,31 @@ def decrypt_payload(server_state, keypair, ratchet_header, encrypted_content, ** - state (bytes): Updated server state. - error (Exception or None) """ + publish_shared_key = kwargs.get("publish_shared_key") + publish_pub_key = kwargs.get("publish_pub_key") + try: if not server_state: state = States() + Ratchets.bob_init(state, publish_shared_key, publish_keypair) + logger.info("Ratchet initialized successfully.") else: state = States.deserialize(server_state) - publish_shared_key = kwargs.get("publish_shared_key") - client_pub_key = kwargs.get("client_pub_key") - - Ratchets.bob_init(state, publish_shared_key, keypair) - logger.info("Ratchet initialized successfully.") - - header = HEADERS(keypair) - header.deserialize(ratchet_header) + header = HEADERS.deserialize(ratchet_header) logger.info("Header deserialized successfully.") - plaintext = Ratchets.decrypt(state, header, encrypted_content, client_pub_key) + plaintext = Ratchets.decrypt(state, header, encrypted_content, publish_pub_key) logger.info("Content decrypted successfully.") return plaintext, state, None - except (struct.error, IndexError, base64.binascii.Error) as e: + except Exception as e: + logger.error("Error decrypting relaysms payload: %s", e, exc_info=True) return None, None, e def encrypt_payload( - server_state, publish_shared_key, client_pub_key, content, **kwargs + server_state, publish_shared_key, client_publish_pub_key, content, **kwargs ): """ Encrypts content into a RelaySMS payload. @@ -66,7 +67,7 @@ def encrypt_payload( Args: server_state (bytes): Current state of the server-side ratchet. publish_shared_key (bytes): Publish shared key. - client_pub_key (bytes): Client's public key for encryption. + client_publish_pub_key (bytes): Client's public key for encryption. content (str): Plaintext content to encrypt. kwargs (dict): Additional keyword arguments: - client_keystore_path (str): Path to client's keystore. @@ -78,10 +79,14 @@ def encrypt_payload( - state (bytes): Updated server state. """ state = States.deserialize(server_state) - client_keystore_path = kwargs.get("client_keystore_path") + server_keystore_path = kwargs.get("server_keystore_path") - Ratchets.alice_init(state, publish_shared_key, client_pub_key, client_keystore_path) - header, content_ciphertext = Ratchets.encrypt(state, content, client_pub_key) + Ratchets.alice_init( + state, publish_shared_key, client_publish_pub_key, server_keystore_path + ) + header, content_ciphertext = Ratchets.encrypt( + state, content, client_publish_pub_key + ) return header.serialize(), content_ciphertext, state @@ -114,7 +119,8 @@ def decode_relay_sms_payload(content): return header, encrypted_content, None - except (struct.error, IndexError, base64.binascii.Error) as e: + except Exception as e: + logger.error("Error decoding relaysms payload: %s", e, exc_info=True) return None, None, e diff --git a/src/utils.py b/src/utils.py index 30d9633..0fcc685 100644 --- a/src/utils.py +++ b/src/utils.py @@ -262,35 +262,6 @@ def get_shared_key(keystore_path, pnt_keystore, secret_key, peer_pub_key): return shared_key -def error_response(context, response, sys_msg, status_code, user_msg=None, _type=None): - """ - Create an error response. - - Args: - context: gRPC context. - response: gRPC response object. - sys_msg (str or tuple): System message. - status_code: gRPC status code. - user_msg (str or tuple): User-friendly message. - _type (str): Type of error. - - Returns: - An instance of the specified response with the error set. - """ - if not user_msg: - user_msg = sys_msg - - if _type == "UNKNOWN": - logger.exception(sys_msg, exc_info=True) - else: - logger.error(sys_msg) - - context.set_details(user_msg) - context.set_code(status_code) - - return response() - - def encrypt_and_encode(plaintext): """ Encrypt and encode plaintext. diff --git a/tests/sample.py b/tests/sample.py deleted file mode 100644 index cff6704..0000000 --- a/tests/sample.py +++ /dev/null @@ -1,83 +0,0 @@ -import base64 -import struct -import grpc - -import vault_pb2 -import vault_pb2_grpc - -from smswithoutborders_libsig.keypairs import x25519 -from smswithoutborders_libsig.ratchets import Ratchets, States -from src.device_id import compute_device_id - - -def main(): - try: - channel = grpc.insecure_channel("localhost:6000") - - with channel as conn: - stub = vault_pb2_grpc.EntityStub(conn) - - client_path = "/home/megamind/promisefru/sample.db" - client_path_1 = "/home/megamind/promisefru/sample1.db" - keypair_obj = x25519(client_path) - peer_pub_key = keypair_obj.init() - - a_request_data = { - "phone_number": "+23712345678900", - "ownership_proof_response": "123456", - "country_code": "CM", - "password": "Password@1234", - "client_publish_pub_key": base64.b64encode(peer_pub_key).decode( - "utf-8" - ), - "client_device_id_pub_key": base64.b64encode(peer_pub_key).decode( - "utf-8" - ), - } - - a_request = vault_pb2.CreateEntityRequest(**a_request_data) - a_response = stub.CreateEntity(a_request) - - print(a_response) - - server_pub = base64.b64decode(a_response.server_publish_pub_key) - sk = keypair_obj.agree(server_pub) - - original_plaintext = b"Hello world" - client_state = States() - Ratchets.alice_init( - client_state, - sk, - server_pub, - client_path_1, - ) - header, client_ciphertext = Ratchets.encrypt( - client_state, - original_plaintext, - server_pub, - ) - - len_header = len(header) - transmission_text = base64.b64encode( - struct.pack("