Skip to content

Security Issues

Cody Newberry edited this page Oct 28, 2022 · 2 revisions

Client and server communication:

A diagram is available at ./Documents/UML Neptune - ServerClient Negotiation.pdf

With the current design, utilizing a Diffie-Hellman key exchange, a hash-based key derivation function, a pre-shared “pair key” (that was shared via an encrypted manager) and HTTPS during the key exchange, if the exchange is implemented correctly, no issues should arise with the key exchange between the server and client. We do not have to worry about MITM attacks if we utilize HTTPS.

  1. Client initiates communication with the server by sending a POST request, over HTTPS, to /api/v1/client/newSocketConnection/. In this request the client sends the following data:

    • acceptedKeyGroups: DH key groups the client will accept
    • acceptedHashTypes: hash functions the client will accept (SHA128/SHA256/SHA512)
    • acceptedCrypto: crypto functions the client will accept (AES256-GCM, ChaCha20, AES256-CBC, AES128-GCM, AES128-CBC)
    • useDynamicSalt: use separate DH exchange to derive the salt used to derive the AES key/iv.
  2. The server receives the data, and beings to create two pairs of public modulus (g), public bases (p), and it’s public key (A). Only one set is generate if useDynamicSalt is set to false. It then replies to the POST request with the following:

    • g1: base There are two, second set is optional and used to derive a salt.
    • p1: modulus
    • A1: server's public integer
    • conInitId: random string of length 16, used to identify the client's next call\
    • g2, p2, A2: Are the base, modulus, and server's public integer for deriving a shared dynamic salt (if useDynamicSalt was true in the client's request).
  3. Client then calculates it’s public key pairs, then uses that to calculate the shared secret via the Diffie-Hellman key exchange. Using this shared secret and the HDKF function, we generate the shared AES key and IV. If useDynamicSalt is true, we utilize the second DH shared secret and HKDF to derive a shared key that is then passed as the salt into the HKDF function for the AES key generation. We always pass pairKey (a shared secret) as the outer most salt/pepper. We then generate a random string (chkMsg) of length 64, save its SHA256 hash as (chkMsgHash) and finally encrypt chkMsg. This is used to later validate the key derived on both sides matches. We then send another HTTPS POST request but this time utilize the conInitId: /api/v1/client/newSocketConnection/{conInitId}. We send the following:

    • B1: client's public key
    • B2: client's public key (dynamic salt)
    • pairId: a shared secret id the client and server setup on first pair, data is encrypted. Tells the server which client we are, used in deriving the salt used to create the AES key and iv. Provides the server with an assurance that we've talked before. If a client and server have not paired before, this will be empty.
    • newPair: if the server and client have not paired before, this will be true, otherwise false or exempt. Singles that a pair request will follow socket connection.
    • chkMsg: encrypted random string of length 64. Encrypted using the derived AES key and iv created from the shared secret (DH). Used to validate encryption (both parties have the same key).
    • chkMsgHash: hash (SHA256/SHA1) of the decrypted chkMsg string.
    • chkMsgHashFunction: sha256, sha1, md5. Function used to create chkMsgHash.
  4. Server calculates and derives the shared AES key and IV using HKDF and the same salts as the client. Server then decrypts the chkMsg, verifies it matches the chkMsgHash and then calculates confMsg = SHA256(chkMsg + chkMsgHash). This confMsg is used to tell the server that we can decrypt data and encrypt it, that we have the same keys. We then generate a random socketId (string of length 16) and create a new socket using that id. We send the following back to the server:

    • socketId: the socket id the client needs to connect to (/api/v1/client/socket/{socketId}).
    • confMsg: hash of the decrypted chkMsg concatenated with the chkMsgHash. Used to tell the client the server can decrypt and encrypt.
  5. Client then connects to the socket: /api/v1/client/socket/{socketId} and finalizes the connection setup:

    1. If already pair we send:

      • command: /api/v1/client/newClientConnection`,
      • clientId: the client identifier (this is who we are)
      • TTL: time-to-live, how long until we require a renegotiation. (Client’s maximum accepted time).
    2. and the server replies with:

      • command: /api/v1/client/newClientConnection,
      • TTL: decided time-to-live (will be <= client's requested TTL)
      • connectionId: a unique identifier to represent this client connection.
    3. If a new pair needs to be setup, we send:

      • command: /api/v1/client/newPairRequest,
      • TTL: time-to-live, how long until we require a renegotiation. (our maximum accepted time).
      • clientId: the client identifier (unique, hopefully)
      • friendlyName: the client's display name (John's Phone)
      • listeningIpAddress: likely the current IP of the client device, whatever the SocketServer is bound to
      • listeningPort: port SocketServer is bound to configuration: the configuration data, JSON key value pair. Similar to /api/v1/client/updateConfiguration.
    4. and the server replies with:

      • command: /api/v1/client/newPairResponse,
      • pairId: the unique identifier to represent a pair relationship between two devices (server and client)
      • pairKey: a shared key used in encryption (think of this as the salt or the pepper used to derive the AES keys). Only the server and client know this. Persistent for as long as the devices are paired, however, can be changed via a request.
      • TTL: decided time-to-live (will be <= client's requested TTL)
      • connectionId: a unique identifier to represent this client connection.

At any time the client can send a scrap request to halt the connection /api/v1/client/newSocketConnection/{conInitId}/scrap. The server will delete any socket and remove any references.


Stored data:

Android:

Android provides easy-to-use API calls and examples for storing configuration data in an encrypted manner. Our client app will utilize these APIs to store all configuration data, especially pair keys, encrypted on the filesystem. Additionally, since Android by nature is more “locked down,” we can expect our app data to be hidden from the user and their apps.

Server:

Many major operating systems provide system data protection APIs and keychain APIs. There exists a Node package, keytar that allows cross-platform, easy to use, API calls to interact with these OS keychains. For macOS that would be Keychain, Linux is the `Secret Service API/libsecret” and Window’s Credential Vault. Utilizing the keychain APIs, we can store an encryption key used to protect the server’s configuration data. This way the encryption key can be stored safely with the OS, and our configuration data can be decrypted without prompting the user to enter a password.

For Window specifically, we can take things one step further. Windows includes a special Data Protection API (DPAPI) that utilizes the user’s password and the computer’s keys to encrypt streams of plaintext into chiphertext. This way we do not have to worry about store the encryption key, the encryption key is never seen by our program at all. We can increase the protection provided by the DPAPI by using a salt when encryption/decrypting data and storing that salt using the Credential Vault.

Only downside is if the computer has been compromised in some manner, then another program could utilize these same methods to retrieve keys and decrypt encryption data. However, this is quite hard to protect against being an open-source program.


Compromised system:

Full system compromised:

For either application, a full system compromise is not a part of our attack vector. If your system is absolutely compromised, I.E. root access or System access there’s little to nothing we can do other than self-destroy or refuse to run. Additionally, if a user is compromised to this degree, they have bigger issues.

Filesystem compromised:

In the case of the server, if a rogue application has access to the filesystem, but not the keychain, they’ll be unable to grab the encryption key used to decrypt the data.
In the case of the client, this would require root level access, but additionally they would be unable to access the encryption keys stored by Android.