Skip to content

Security Model

ivanab edited this page Nov 27, 2016 · 9 revisions

Node JS server start-up

When a the nodejs server is loaded:

  1. a cryptographic server-secret-id random string is generated
  2. a cryptographic server-secret random string is generated (this secret is never sent to the client)
  3. email-server-secret this is a hardcoded email server secret known both by the nodejs servers and the invitation email server

Server side home page load

When the home page loads:

  1. a server-session-id random string is generated by the server (and is given to the client javascript)
  2. a server-session-id-validation string is generated by the server (and is given to the client javascript) using the following algorithm: hash(server-secret-id + ":" + server-session-id + ":" + server-secret)
  3. the previously generated server-secret-id is sent to the javascript client
  4. a cryptographic server-session-salt random string is generated by the server (and is given to the client javascript)
  5. a cryptographic server-session-secret random string is generated by the server (and is given to the client javascript)
  6. a cryptographic client-session-secret random string is generated by the client javascript (which is NOT sent to the server)

NOTE: The server-secret is NEVER given to the javascript client.

The end-user picks a question from the preset questions (or types their own). The answer is then normalized.

Client side answer normalization

The answer normalization process:

  1. strip all pre/post white space from the string
  2. collapse all white space between words to a single space
  3. collapse spaces before and after and - symbol
  4. make all characters uppercase (or perhaps lowercase; doesn't matter but we have to be consistent)
  5. remove any trailing punctuation (no . or ?)
  6. ensure the minimum 4 character rule is satisfied

NOTE: All of this normalization processing must be done entire on the client side javascript as the server will never receive the answer.

Client side answer proof generation

The javascript client must generate proof for the remote party to prove that the remote party's javascript client indeed know the correct answer.

Generating proof of the correct answer:

answer-proof = hash("answer:" + server-secret-id + ":" + server-session-id + ":" + server-session-id-validation + ":" + server-session-salt + ":" + server-session-secret + ":" + client-session-secret + ":" + answer + ":end")

This proof is used to validate both client know the same answer.

Client-to-client shared secret

Generate the client-to-client encryption/decryption cipher key:

shared-secret = hash("cipher:" + server-secret-id + ":" + server-session-id + ":" + server-session-id-validation + ":" + server-session-salt + ":" + server-session-secret + ":" + client-session-secret + ":" + answer + ":end")

This shared secret cipher key is the master key used to encrypt or decrypt data send between two clients. The server does not have the client-session-key and thus the server will not be able to decrypt any data without this key.

Local client creates a new chat session

Client sends the following information to the server to create a new session:

  1. server-session-id
  2. server-session-id-validation
  3. server-session-salt
  4. server-session-key
  5. answer-proof
  6. The question (encrypted) + question salt + validation:

question-salt = cryptographically randomly generated string on client
encrypted-question = encrypt(question-secret, question)
question-secret = hash("secret:" + question-salt + ":" + client-session-secret)
question-secret-validation = hash("validate:" + question-salt + ":" + client-session-secret)
question-integrity = hmac(question-secret, plain-text-question)

  1. The message (encrypted) + message salt + validation:

message-salt = cryptographically randomly generated string on client
encrypted-message = encrypt(message-secret, message)
message-secret = hash("secret:" + message-salt + ":" + shared-secret)
message-secret-validation = hash("validate:" + message-salt + ":" + shared-secret)
message-integrity = hmac(message-secret, plain-text-message)

  1. To/from email/name information

NOTE: Each message sent will get a brand new randomly generated salt. The question-secret and the message-secret are never sent to the server.

The server will validate the creation by verifying:

  1. The server-session-id is brand new (this must be an atomic check)
  2. Generates check-server-session-id-validation and compares equal-to server-session-id-validation

check-server-session-id-validation = hash(server-secret-id + ":" + server-session-id + ":" + server-secret)

The server will store the client provided information into a session record and return the following information:

  1. email-server-nonce - a random string generated by the server
  2. email-server-secret-proof = hash("email-proof:" + email-server-secret + ":" + email-server-nonce + ":" + email-server-secret-expiry + ":" + to-email-address)
  3. email-server-secret-expiry - a unix epoch based time stamp of when this email proof will expire.

Forming and sending the remote party the chat session URL

The URL to share with the remote party will contain the following:
chat-url = https://trustinchat.com/path/server-session-id#client-session-secret

As the information following the anchor (#) on a URL is not supposed to be sent by a browser to a server (but can be read by JavaScript), the remote client JavaScript will have a piece of information in the URL never sent to the server which is part of the message secret.

The concern is this URL must be sent to the remote party in the invitation email and that could mean the server has access to the client-session-secret which is part of the URL to be emailed. To minimize this risk the request to send the invitation email is an independent request to the server sent to an entirely different and independent server machine.

The request to send the invitation email must include the following:

  1. To/from email/name information
  2. chat-url
  3. email-server-nonce - a random string generated by the server
  4. email-server-secret-proof = hash("email-proof:" + email-server-secret + ":" + email-server-nonce + ":" + email-server-secret-expiry + ":" + to-email-address)
  5. email-server-secret-expiry - a unix epoch based time stamp of when this email proof will expire.

The server will perform the following validation steps:

  1. Check to see that the email-server-nonce has never been used before (this check must be atomic).
  2. Ensure check-email-server-secret-proof equals email-server-secret-proof where check-email-server-secret-proof = hash("email-proof:" + email-server-secret + ":" + email-server-nonce + ":" + email-server-secret-expiry)
  3. Ensure the email-server-secret-expiry has not already expired.

If this information all passes, the invitation email is sent.

Receiving the first message

The remote party will have to prove to the server that it has the correct answer. When the page is loaded with the session id the following information is sent in the remote user's javascript page:

  1. server-session-id
  2. server-session-id-validation
  3. server-session-salt
  4. server-session-key
  5. The question (encrypted) + question salt:

question-salt = cryptographically randomly generated string on client
encrypted-question = encrypt(question-secret, question)
question-secret = hash("secret:" + question-salt + ":" + client-session-secret)
question-validation = hash("validate:" + question-salt + ":" + client-session-secret)
question-integrity = hmac(question-secret, plain-text-question)

NOTE: The message and message salt are NOT included. The proof must fist be provided of the correct answer to the server. The server will enforce the number of attempts has not been exceeded. If the number of attempts are exceeded the message is destroyed.

The client Javascript will decrypt the question and present to the end user if question-validation matches against check-question-validation = hash("validate:" + question-salt + ":" + client-session-secret). The question once decrypted integrity is then checked to ensure the decrypted message has been decrypted properly before being presented to the end user with check-question-integrity = hmac(question-secret, plain-text-question).

If the client Javascript cannot validate the check-question-validation against the question-validation then the session is assumed invalid and an error is presented to the end user.

When the user enters the answer the answer passes through the same validation process as when the answer was first created. This ensures both users have entered the same information.

The JavaScript must send this to the server to provide evidence it knows the correct answer:

  1. answer-proof = hash("answer:" + server-secret-id + ":" + server-session-id + ":" + server-session-id-validation + ":" + server-session-salt + ":" + server-session-secret + ":" + client-session-secret + ":" + answer + ":end")

If this matches the answer-proof the server has stored for the session then sent to the client:

  1. The message (encrypted) + message salt + validation:

message-salt = cryptographically randomly generated string on client
encrypted-message = encrypt(message-secret, message)
message-secret = hash("secret:" + message-salt + ":" + shared-secret)
message-validation = hash("validate:" + message-salt + ":" + shared-secret)
message-integrity = hmac(message-secret, plain-text-message)

The Javascript client then knows it has the correct answer and can generate the shared-secret:

shared-secret = hash("cipher:" + server-secret-id + ":" + server-session-id + ":" + server-session-id-validation + ":" + server-session-salt + ":" + server-session-secret + ":" + client-session-secret + ":" + answer + ":end")

The Javascript client can then validate the message-validation in the same manner as the question validation and if it passes then decrypt the message then validate the decrypted message integrity and display the message to the end user.

Sending additional messages

Each subsequent message sent between users uses the shared-secret to generate new message-salt for each message which can be used to create a new encrypted message. The message-salt must NEVER be reused twice, thus the random salt must be truly cryptographically randomly generated and sufficiently long in number of characters.

The new message (encrypted) + message salt + validation:

new-message-salt = cryptographically randomly generated string on client
new-encrypted-message = encrypt(message-secret, message)
new-message-secret = hash("secret:" + message-salt + ":" + shared-secret)
new-message-validation = hash("validate:" + message-salt + ":" + shared-secret)
new-message-integrity = hmac(new-message-secret, new-plain-text-message)

The server's job is then to relay the encrypted messages and temporarily store the encrypted versions of the messages on the server until the other party has verified the message has been delivered to the end user.

Other considerations

  1. Chat sessions should have a maximum lifetime from the point of creation.
  2. A cookie can be used to store the answer-proof and shared-secret for either party when it is certain it has the correct information. This might be needed to handle page reload logic.
  3. Not all information needed to do the server messaging is included in this document. Only the security aspects are included to illustrate how to create a secure chat session.