Skip to content

knoxen/SrpcWorld

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

13 Commits
 
 
 
 
 
 
 
 
 
 

Repository files navigation

SrpcWorld

A Secure Remote Password Cryptor Demo for Elixir

The Secure Remote Password Cryptor (SRPC) is a security framework based on the Secure Remote Password (SRP) protocol. SRPC builds on SRP to provide application-layer security for client/server communications. This provides a single entity trust model (no trusted third party) with mutual authentication and a host of other features. SRPC is transport independent. This demo uses HTTP.

The primary purpose of this demo is to show the touch points for Elixir application integration of the SRPC framework. Secondarily, the demo can be configured to use a client-side proxy to examine the structure of the SRPC messaging packets.

The SrpcWorld demo consists of the following server and client applications:

  • SrpcWorld.Server provides implementation for the following routes

    • Simple HTTP requests
      • /hello – says hello to the name specified in a URL request query string
      • /reverse – reverses data provided in a URL request
    • HTTP JSON API
      • /status – on/off status of a set of lights
      • /action – control the on/off state of the lights
  • SrpcWorld.Client provides a simple API to access SrpcWorld.Server simple HTTP requests

  • SrpcWorld.Lights provides a simple API to access SrpcWorld.Server HTTP JSON API

SRPC framework functionality is added through SrpcPlug on the server side and SrpcClient on the client side.

Table of Contents

Install Elixir

The Elixir language organization has detailed information regarding the installation of Elixir.

[Table of Contents](#TOC)

Compile SrpcWorld

In each of the client and server directories,

mix deps.get
mix compile
[Table of Contents](#TOC)

Start SrpcWorld

In the server directory:

> iex -S mix
…
server ∴

In the client directory:

> iex -S mix
…
client ∴

Each directory contains an .iex.exs file that sets the IEx prompt to either client or server, followed by a ∴ sign.

[Table of Contents](#TOC)

Run SrpcWorld

HTTP calls

SrpcWorld.Client provides an API to access SrpcWorld.Server. For example, SrpcWorld.Client.say_hello/1 sends an HTTP GET /hello request to the server. However, we want this communication to be secure, so SrpcWorld.Client actually uses the SRPC framework to make the call. First, let's say hello to "Elixir" in the client IEx shell.

client ∴ SrpcWorld.Client.say_hello("Elixir")
10:02:21.703 [info]  Connected to http://localhost:8082
"Aloha Elixir"

The SrpcWorld.Server is apparently vacationing in Hawai‘i as we see the say_hello response is Aloha Elixir.

The info log message received before the say_hello response informs us that the client connected to the SrpcWorld.Server at the specified URL. Whenever the SrpcWorld.Client makes a call, it reuses an SRPC connection maintained in its GenServer state. Since there was no existing connection, one was created in SrpcWorld.Client with the call:

  {:ok, conn} = SrpcClient.connect()

SrpcClient.connect/0 returns a SrpcClient.Connection that is used to send secure messages to the SrpcWorld.Server. The connection is passed as the first parameter to SrpcClient functions such as in the implementation of SrpcWorld.Client.say_hello/1:

  conn
  |> SrpcClient.get("/hello?name=#{name}")

Subsequent SrpcWorld.Client calls reuse the same SrpcClient.Connection:

client ∴ SrpcWorld.Client.reverse("Stressed was I ere I saw desserts")
"stressed was I ere I saw dessertS"
Controlling lights

SrpcWorld.Server fronts a set of virtual lights that can be controlled using the client-side SrpcWorld.Lights module. Controlling the lights requires a valid user login. The demo server includes setup of user srpc with password secret. To login, we use SrpcWorld.Lights.login/2:

client ∴ SrpcWorld.Lights.login("srpc", "secret")
:ok

If you've configured a proxy to observe SRPC traffic you'll see that no information regarding the user ID or password is visible (or leaks) during either SRPC registration or login messaging. In fact, the user password never leaves the client. The Under the Hood section describes what data is actually used to authenticate a user.

Let's get the status of the lights:

client ∴ SrpcWorld.Lights.status()
%{"green" => "off", "red" => "off", "yellow" => "off"}

All the lights are off. Let's turn the red light on:

client ∴ SrpcWorld.Lights.on("red")
%{"green" => "off", "red" => "on", "yellow" => "off"}

Now let's turn the green light on:

client ∴ SrpcWorld.Lights.on("green")
%{"green" => "on", "red" => "on", "yellow" => "off"}

Note the red light stayed on. We could use SrpcWorld.Lights.off/1 to turn the red light off, or we could use SrpcWorld.Lights.switch/1 which switches to a particular light and turns all others off:

client ∴ SrpcWorld.Lights.switch("yellow")
%{"green" => "off", "red" => "off", "yellow" => "on"}

In each of the above calls, SrpcWorld.Lights is sending HTTP JSON API requests to SrpcWorld.Server using SrpcClient.post/2. All the calls are secured via the SRPC framework.

[Table of Contents](#TOC)

Inspect Traffic

The demo can be configured to use a proxy to inspect the traffic between the SrpcWorld.Client and SrpcWorld.Server. Using such a proxy will reveal that all SRPC calls "look" identical. Each call is an HTTP POST to the URL http://host:port/ (where the host and port are specified in the configuration files). Neither the request nor response headers contain any meaningful demo application information. And the request and response bodies are encrypted. The bodies of an example request (top) and response (bottom) looks like:

[Table of Contents](#TOC)

Configure SrpcWorld

Most of the demo configuration should not be changed. However, values of interest that can be changed on the server are host and port and on the client are port and an optional proxy setting.

SrpcWorld.Server

Server-side settings are in server/config/config.exs.

:srpc_plug

Server-side SRPC framework functionality is provided by the SrpcPlug module. SrpcPlug shares a pre-defined relationship with the SrpcClient module used by SrpcWorld.Client. The server portion of this pre-defined relationship is contained in the server/priv/server.srpc file and should not be altered. The SRPC framework also requires a module that provides the SrpcHandler behaviour (to maintain server-side state for the SRPC framework).

config :srpc_plug,
  srpc_file: "priv/server.srpc",
  srpc_handler: SrpcWorld.Server.SrpcHandler

SrpcPlug is compiled with a time-limited demo of the SRPC framework that is valid for just over 3 hours after server startup. If the SrpcWorld.Server attempts to process a message through SrpcPlug after running for more than this time limit the plug will return a custom Fahrenheit 451 status code. Per the Plug documentation, this custom code status must be specified in the server configuration as follows:

config :plug, :statuses, %{
  451 => "Srpc Demo Expired"
}

The SrpcWorld demo uses Cowboy for its HTTP server.

:cowboy
config :cowboy,
  cowboy_opts: [
    port: 8082,
    acceptors: 5
  ]

The SrpcWorld.Server.SrpcHandler module uses EntropyString to generate random connection IDs. These settings configure the number of entropy bits in the ID, as well as the characters used.

:entropy_string
config :entropy_string,
  bits: :session,
  charset: :charset32

The SrpcWorld.Server.SrpcHandler module uses kncache (a simple cache) for maintaining server-side data. These settings configure the necessary caches and default TTLs.

:kncache
config :kncache,
  caches: [
    srpc_exch: 30,
    srpc_nonce: 35,
    srpc_conn: 3600,
    srpc_reg: 3600,
    user_data: 3600
  ]
SrpcWorld

Client-side settings are in client/config/config.exs.

Client-side SRPC framework functionality is provided by the SrpcClient module. SrpcClient shares a pre-defined relationship with the SrpcPlug module used by SrpcWorld.Server. The client portion of this pre-defined relationship is contained in the client/priv/client.srpc file and should not be altered.

SRPC is transport independent. This demo uses SrpcPoison, which provide HTTP transport via HTTPoison.

:srpc_client
config :srpc_client,
  srpc_file: "priv/client.srpc",
  transport: SrpcPoison

SrpcWorld clients connect to the SrpcWorld.Server on a specified host and port.

config :srpc_client, :server,
  host: "localhost",
  port: 8082
:srpc_poison

An optional proxy configuration allows HTTP traffic to be channeled through a proxy for inspection purposes. This setting is passed to HTTPoison as a request option. See HTTPoison for further information. Setting the proxy configuration would look like:

config :srpc_poison, proxy: "http://localhost.charlesproxy.com:8888"
[Table of Contents](#TOC)

Under the Hood

Both SrpcWorld.Client and SrpcWorld.Lights use an SrpcClient.Connection to communicate securely with SrpcWorld.Server. SrpcWorld.Client does not require a valid user login but still ensures mutual authentication between the SrpcClient being used by SrpcWorld.Client and the SrpcPlug being used by SrpcWorld.Server. This mutual authentication is achieved using values in the files specified by the :srpc_file configuration of the client and server. These values form the SRP pre-defined relationship between the SRPC client and server processes.

[Table of Contents](#TOC)
SRPC Lib Connection

Let's take a peek under the hood by creating an SrpcClient.Connection. It would be best to restart both the client and server applications to clean out any residue from earlier demo activity. The client and server IEx sessions can each be terminated by using Cntl-C twice.

client ∴ SrpcClient.connect()
{:ok, #PID<0.226.0>}

SrpcClient.connect/0 returns {:ok, conn_pid} for a successful connection attempt. We created a connection, but didn't grab it. Let's fix that:

client ∴ {:ok, conn} = SrpcClient.connect()
{:ok, #PID<0.226.0>}

Because the SrpcClient.connect/0 call does not specify user credentials, the connection returned is an SRPC lib connection (i.e., a connection that has mutually authenticated the SRPC libraries in use). We can get information regarding the connection using SrpcClient.info/1:

client ∴ SrpcClient.info(conn)
%SrpcClient.Conn.Info{
  accessed: 12,
  count: 0,
  created: 12,
  keyed: 12,
  name: :LibConnection_2
}

The info reports how long ago (in seconds) the connection was created, accessed, and keyed, as well as the connection name and a count of the number of time used. Note the conn reference actually holds the second connection created since we didn't capture the first.

We can use the connection by sending it to various SrpcClient calls. For example, to POST a request to reverse the string string:

client ∴ conn |> SrpcClient.post("/reverse", "string")
{{:ok, "gnirts"}, #PID<0.228.0>}

Inspecting the info again, we'll see the connection has been updated:

client ∴ SrpcClient.info(conn)
%SrpcClient.Conn.Info{
  accessed: 11,
  count: 1,
  created: 32,
  keyed: 32,
  name: :LibCon

Let's close the connection and get a new one:

client ∴ SrpcClient.close(conn)
:ok

client ∴ {:ok, conn} = SrpcClient.connect()
{:ok, #PID<0.304.0>}

client ∴ SrpcClient.info(conn)
%SrpcClient.Conn.Info{
  accessed: 11,
  count: 0,
  created: 11,
  keyed: 11,
  name: :LibConnection_3
}

client ∴ :observer.start

The SrpcClient application structure at this point looks like:

There are two connections. LibConnection_3 is referenced by our current conn, whereas LibConnetion_1 is the first connection we didn't capture. Let's clean up a bit and close that dangling connection. SrpcClient.connections/0 returns a list of {atom, pid} pairs which we can use to close LibConnection_1:

client ∴ SrpcClient.connections() |> Keyword.get(:LibConnection_1) |> SrpcClient.close()
:ok

We can get detailed information regarding a connection using SrpcClient.info/2:

client ∴ conn |> SrpcClient.info(:full)
%SrpcClient.Conn{
  accessed: -576460526,
  conn_id: "nPF4MDLrMp8PjNBpb6tBhQfjd6",
  created: -576460526,
  crypt_count: 0,
  entity_id: "srpc_demo_Gc6kmLMM",
  keyed: -576460526,
  name: :LibConnection_3,
  pid: #PID<0.304.0>,
  reconnect: nil,
  req_mac_key: <<212, 250, 216, 31, 26, 224, 23, 3, 154, 87, 90, 226, 33, 113,
    192, 47, 35, 46, 90, 242, 111, 96, 133, 149, 27, 172, 76, 134, 150, 213,
    148, 185>>,
  req_sym_key: <<159, 105, 54, 5, 199, 44, 118, 103, 123, 129, 227, 232, 78, 30,
    38, 17, 138, 20, 238, 218, 180, 173, 27, 56, 112, 150, 202, 170, 183, 79,
    160, 156>>,
  resp_mac_key: <<2, 78, 228, 4, 64, 6, 184, 181, 253, 145, 255, 10, 204, 12,
    200, 146, 6, 16, 35, 176, 65, 3, 240, 71, 172, 193, 252, 79, 155, 138, 91,
    82>>,
  resp_sym_key: <<222, 238, 153, 33, 128, 84, 126, 48, 242, 3, 162, 70, 253, 67,
    11, 9, 164, 118, 204, 250, 210, 149, 215, 94, 138, 33, 235, 68, 1, 247, 182,
    164>>,
  sha_alg: :sha256,
  sym_alg: :aes256,
  time_offset: 0,
  type: :lib,
  url: "http://localhost:8082"
}

The information includes four binary keys. The *_sym_keys are used for encryption (confidentiality) and the *_mac_keys are used for message authentication codes (data integrity and origin). Distinct keys are used in each direction of messaging.

Let's see how this connection info is handled on the server side. For the SrpcWorld demo, server-side SRPC processing is handled by the SrpcPlug module. There is an :srpc_srv module (written in Erlang) but it is a library, not an application, and hence has no state. It is up to the server-side application to provide necessary SRPC state management via a module providing the :srpc_handler behaviour. In the SrpcWorld demo, the configured SRPC handler is SrpcWorld.Server.SrpcHandler.

To view the server-side information for the LibConnection_3 connection, switch over to the server IEx shell and use the conn_id from the SrpcClient.info/2 listing above:

server ∴ SrpcWorld.Server.SrpcHandler.get_conn("nPF4MDLrMp8PjNBpb6tBhQfjd6")
{:ok,
 %{
   conn_id: "nPF4MDLrMp8PjNBpb6tBhQfjd6",
   entity_id: "srpc_demo_Gc6kmLMM",
   req_mac_key: <<212, 250, 216, 31, 26, 224, 23, 3, 154, 87, 90, 226, 33, 113,
     192, 47, 35, 46, 90, 242, 111, 96, 133, 149, 27, 172, 76, 134, 150, 213,
     148, 185>>,
   req_sym_key: <<159, 105, 54, 5, 199, 44, 118, 103, 123, 129, 227, 232, 78,
     30, 38, 17, 138, 20, 238, 218, 180, 173, 27, 56, 112, 150, 202, 170, 183,
     79, 160, 156>>,
   resp_mac_key: <<2, 78, 228, 4, 64, 6, 184, 181, 253, 145, 255, 10, 204, 12,
     200, 146, 6, 16, 35, 176, 65, 3, 240, 71, 172, 193, 252, 79, 155, 138, 91,
     82>>,
   resp_sym_key: <<222, 238, 153, 33, 128, 84, 126, 48, 242, 3, 162, 70, 253,
     67, 11, 9, 164, 118, 204, 250, 210, 149, 215, 94, 138, 33, 235, 68, 1, 247,
     182, 164>>,
   sha_alg: :sha256,
   sym_alg: :aes256,
   type: :lib
 }}

Note the conn_id, entity_id, type and cryptographic keys are identical on each side of the connection.

[Table of Contents](#TOC)
SRPC User Registration

On the client, let's register a new user:

client ∴ SrpcClient.register("chigurh", "call it")
:ok

and then look at what information is received and maintained by the server:

server ∴ SrpcWorld.Server.SrpcHandler.get_registration("chigurh")
{:ok,
 %{
   kdf_salt: <<111, 86, 19, 83, 159, 172, 246, 136, 209, 166, 225, 167>>,
   srp_salt: <<145, 111, 37, 210, 141, 70, 10, 110, 205, 40, 104, 204, 30, 230,
     104, 151, 194, 218, 247, 149>>,
   user_id: "chigurh",
   verifier: <<126, 98, 148, 155, 243, 98, 176, 154, 54, 24, 181, 75, 200, 123,
     241, 114, 37, 93, 85, 54, 198, 144, 124, 231, 222, 244, 96, 110, 208, 251,
     130, 175, 190, 96, 140, 27, 131, 251, 27, 95, 241, 178, 238, 154, 143, 94,
     136, 223, 241, 206, 214, 3, 44, 127, 246, 179, 93, 48, 202, 243, 211, 13,
     197, 193, 134, 180, 171, 75, 55, 142, 31, 40, 85, 191, 204, 142, 127, 61,
     163, 239, 219, 38, 193, 119, 216, 223, 8, 168, 238, 3, 167, 57, 225, 111,
     143, 64, 115, 208, 183, 245, 110, 148, 244, 145, 77, 181, 158, 13, 180, 60,
     3, 204, 21, 14, 51, 69, 139, 165, 236, 178, 78, 82, 149, 159, 199, 106,
     190, 128, 198, 187, 204, 154, 15, 94, 55, 74, 112, 48, 162, 153, 127, 154,
     245, 93, 13, 244, 103, 237, 107, 96, 164, 24, 100, 117, 138, 110, 134, 245,
     175, 207, 27, 23, 243, 78, 9, 121, 109, 220, 15, 9, 42, 233, 48, 242, 155,
     126, 119, 238, 217, 13, 32, 159, 181, 0, 44, 96, 13, 164, 172, 131, 241,
     155, 185, 5, 93, 33, 230, 117, 2, 14, 46, 43, 62, 124, 216, 18, 250, 199,
     194, 196, 95, 27, 31, 222, 208, 117, 243, 238, 66, 59, 112, 240, 179, 199,
     205, 28, 60, 15, 227, 35, 102, 72, 12, 110, 185, 11, 114, 214, 246, 48, 78,
     154, 85, 154, 29, 15, 46, 148, 241, 254, 101, 182, 84, 52, 71, 250>>
 }}

SrpcClient.register/2 creates random kdf_salt and srp_salt values. kdf_salt is used for PDKDF2 key stretching of the user's password, whereas srp_salt is used to create the user's SRP private key from the stretched password. The private key is then used to calculate the user's verifier. The client sends these values to the server for long-term storage.

[Table of Contents](#TOC)
SRPC User Connection

SrpcClient.connect/2 creates an SRPC connection for a specific user:

client ∴ {:ok, anton} = SrpcClient.connect("chigurh", "call it")
{:ok, #PID<0.246.0>}

Unlike the previous use of SrpcClient.connect/0, which returned an SRPC lib connection, SrpcClient.connect/2 returns an SRPC user connection that is bound to a specific user. Creating such a user connection is the SRPC means of logging in a user. During the connection process, the server sends the user's kdf_salt and srp_salt to the client so the user's SRP private key can be recreated as part of SRPC mutual authentication. This prevents the client from having to maintain any long term user state. SRP's zero-knowledge password authentication scheme means the user's password never leaves the client and hence is never transmitted or known to the server.

Using the SRP protocol, the client and server dynamically calculate a shared secret from random empheral values. This cryptographically strong shared secret is used as keying material to generate the four SRPC connection keys.

User registration and login both require an existing SRPC connection to secure user information during processing. The functions SrpcClient.register/2 and SrpcClient.connect/2 used above automatically create an SRPC lib connection to perform their duties and close the connection when done. Functions SrpcClient.register/3 and SrpcClient.connect/3 accept an existing SrpcClient.Connection as their first argument.

As with previous connections, we can inspect a user connection using SrpcClient.info/1:

client ∴ anton |> SrpcClient.info(:full)
%SrpcClient.Conn{
  accessed: -576460062,
  conn_id: "hNpjfBmmR3pRP7fnhTGG2fgdqT",
  created: -576460062,
  crypt_count: 0,
  entity_id: "chigurh",
  keyed: -576460062,
  name: :UserConnection_1,
  pid: #PID<0.246.0>,
  reconnect: nil,
  req_mac_key: <<235, 135, 142, 163, 59, 22, 190, 42, 225, 27, 174, 142, 103,
    198, 195, 70, 3, 233, 82, 176, 156, 224, 193, 60, 133, 238, 89, 50, 23, 166,
    94, 28>>,
  req_sym_key: <<127, 130, 243, 72, 142, 50, 121, 218, 50, 228, 181, 95, 183,
    38, 23, 221, 12, 196, 226, 212, 74, 219, 251, 31, 54, 112, 40, 82, 83, 220,
    30, 166>>,
  resp_mac_key: <<253, 2, 58, 218, 103, 156, 88, 90, 106, 111, 208, 57, 223,
    129, 46, 191, 98, 101, 8, 58, 216, 82, 207, 198, 42, 171, 110, 120, 110,
    230, 69, 240>>,
  resp_sym_key: <<74, 65, 49, 93, 145, 134, 232, 26, 98, 233, 158, 41, 41, 207,
    29, 8, 189, 81, 148, 70, 192, 215, 145, 79, 26, 231, 99, 201, 2, 26, 139,
    132>>,
  sha_alg: :sha256,
  sym_alg: :aes256,
  time_offset: 0,
  type: :user,
  url: "http://localhost:8082"
}

and on the server:

server ∴ SrpcWorld.Server.SrpcHandler.get_conn("hNpjfBmmR3pRP7fnhTGG2fgdqT")
{:ok,
 %{
   conn_id: "hNpjfBmmR3pRP7fnhTGG2fgdqT",
   entity_id: "chigurh",
   req_mac_key: <<235, 135, 142, 163, 59, 22, 190, 42, 225, 27, 174, 142, 103,
     198, 195, 70, 3, 233, 82, 176, 156, 224, 193, 60, 133, 238, 89, 50, 23,
     166, 94, 28>>,
   req_sym_key: <<127, 130, 243, 72, 142, 50, 121, 218, 50, 228, 181, 95, 183,
     38, 23, 221, 12, 196, 226, 212, 74, 219, 251, 31, 54, 112, 40, 82, 83, 220,
     30, 166>>,
   resp_mac_key: <<253, 2, 58, 218, 103, 156, 88, 90, 106, 111, 208, 57, 223,
     129, 46, 191, 98, 101, 8, 58, 216, 82, 207, 198, 42, 171, 110, 120, 110,
     230, 69, 240>>,
   resp_sym_key: <<74, 65, 49, 93, 145, 134, 232, 26, 98, 233, 158, 41, 41, 207,
     29, 8, 189, 81, 148, 70, 192, 215, 145, 79, 26, 231, 99, 201, 2, 26, 139,
     132>>,
   sha_alg: :sha256,
   sym_alg: :aes256,
   type: :user
 }}

SRPC lib and user connections are identical in terms of use. The only difference is a lib connection represents a mutually authenticated channel between the SRPC framework libraries in use, whereas a user connection also ensures mutual authentication via the user's password (client) and verifier (server). Both user registration and login require an existing SRPC lib or user connection. If a user connection is used, that connection must be rooted in an SRPC lib connection. This ensures no user information is ever transmitted unencrypted.

[Table of Contents](#TOC)
Key Refresh

As noted earlier, SRPC uses separate keys for the encryption and authentication of each message, and a separate pair of keys for messaging in each direction. These four keys can be refreshed at any time. Refreshed keys limit the per key material available for cryptanalysis as well as the vulnerability window of a compromised key.

Let's create and use a new user connection for anton, then send the SrpcServer a few Anton quotes to reverse.

client ∴ {:ok, anton} = SrpcClient.connect("chigurh", "call it")
{:ok, #PID<0.235.0>}

client ∴ [
      "That depends. Do you see me?",
      "If the rule you followed brought you to this, of what use was the rule?",
      "I know where you are.",
      "I won't tell you you can save yourself, because you can't.",
      "Would you hold still, please, sir?",
      "That's foolish. You pick the one right tool."
    ] |> Enum.map(fn quote ->
      {{:ok, reversed}, _} = anton |> SrpcClient.post("/reverse", quote)
      reversed
    end)
["?em ees uoy oD .sdneped tahT",
 "?elur eht saw esu tahw fo ,siht ot uoy thguorb dewollof uoy elur eht fI",
 ".era uoy erehw wonk I",
 ".t'nac uoy esuaceb ,flesruoy evas nac uoy uoy llet t'now I",
 "?ris ,esaelp ,llits dloh uoy dluoW",
 ".loot thgir eno eht kcip uoY .hsiloof s'tahT"]

client ∴ anton |> SrpcClient.info(:full)
%SrpcClient.Conn{
  accessed: -576460736,
  conn_id: "9pp8JJ66PDM9rGMHgJjmjrj8B7",
  created: -576460741,
  crypt_count: 6,
  entity_id: "chigurh",
  keyed: -576460741,
  name: :UserConnection_1,
  pid: #PID<0.235.0>,
  reconnect: nil,
  req_mac_key: <<215, 97, 30, 11, 49, 226, 57, 142, 232, 238, 67, 97, 230, 37,
    64, 30, 34, 51, 132, 31, 209, 230, 114, 110, 172, 186, 73, 255, 99, 86, 68,
    164>>,
  req_sym_key: <<131, 54, 101, 148, 160, 69, 59, 177, 29, 25, 197, 55, 230, 65,
    3, 248, 188, 218, 253, 30, 14, 95, 91, 174, 37, 58, 53, 236, 228, 124, 192,
    13>>,
  resp_mac_key: <<183, 63, 253, 49, 40, 185, 174, 95, 248, 174, 64, 133, 151,
    29, 47, 114, 22, 150, 66, 230, 244, 125, 170, 137, 95, 167, 224, 70, 195,
    135, 152, 238>>,
  resp_sym_key: <<24, 146, 249, 187, 51, 114, 171, 222, 63, 14, 103, 136, 66,
    85, 213, 210, 226, 53, 18, 18, 131, 75, 107, 177, 227, 216, 64, 62, 16, 54,
    76, 9>>,
  sha_alg: :sha256,
  sym_alg: :aes256,
  time_offset: 0,
  type: :user,
  url: "http://localhost:8082"
}

We can see from the crypt_count above the anton connection keys have been used 6 times. Let's manually refresh the keys:

client ∴ SrpcClient.refresh(anton)
:ok

client ∴ anton |> SrpcClient.info(:full)
%SrpcClient.Conn{
  accessed: -576460669,
  conn_id: "9pp8JJ66PDM9rGMHgJjmjrj8B7",
  created: -576460741,
  crypt_count: 0,
  entity_id: "chigurh",
  keyed: -576460669,
  name: :UserConnection_1,
  pid: #PID<0.235.0>,
  reconnect: nil,
  req_mac_key: <<40, 26, 100, 16, 158, 107, 135, 122, 159, 244, 35, 25, 219,
    229, 126, 53, 53, 82, 90, 223, 29, 185, 66, 198, 234, 2, 53, 202, 155, 146,
    122, 253>>,
  req_sym_key: <<227, 247, 9, 74, 57, 203, 101, 125, 181, 150, 245, 60, 98, 164,
    32, 187, 43, 93, 181, 91, 201, 84, 63, 142, 34, 121, 1, 128, 208, 214, 20,
    253>>,
  resp_mac_key: <<144, 34, 16, 34, 200, 223, 64, 217, 34, 105, 7, 21, 113, 134,
    123, 45, 77, 56, 128, 12, 121, 224, 77, 121, 240, 228, 190, 181, 34, 170,
    63, 187>>,
  resp_sym_key: <<103, 176, 194, 216, 251, 243, 248, 219, 3, 254, 143, 217, 239,
    78, 221, 19, 186, 232, 71, 226, 98, 35, 99, 91, 30, 125, 188, 45, 10, 39,
    218, 8>>,
  sha_alg: :sha256,
  sym_alg: :aes256,
  time_offset: 0,
  type: :user,
  url: "http://localhost:8082"
}

After refreshing the keys, the crypt_count is back to zero and the four keys are different than before the refresh.

Manual key refresh is useful, but since SrpcClient is already keeping count it is quite easy to have the keys auto-refreshed after a specified usage. To do so, we need to add a key_limit option to the client configuration file:

config :srpc_client,
  srpc_file: "priv/client.srpc",
  transport: SrpcPoison,
  key_limit: 4

After restarting the client (Cntl-C twice to stop the IEx process), we can create and use a new anton connection as before. Restarting the server is not necessary, but if you do, don't forget to register user chigurh.

client ∴ {:ok, anton} = SrpcClient.connect("chigurh", "call it")
{:ok, #PID<0.275.0>}

client ∴ anton |> SrpcClient.info(:full)
%SrpcClient.Conn{
  accessed: -576460744,
  conn_id: "dMrbdnf7QDdBm6BGFrD84Nf4jQ",
  created: -576460744,
  crypt_count: 0,
  entity_id: "chigurh",
  keyed: -576460744,
  name: :UserConnection_1,
  pid: #PID<0.275.0>,
  reconnect: nil,
  req_mac_key: <<63, 139, 135, 72, 121, 83, 85, 238, 221, 66, 177, 179, 222,
    119, 48, 137, 255, 234, 138, 32, 140, 212, 97, 71, 157, 158, 99, 191, 92,
    140, 242, 146>>,
  req_sym_key: <<61, 186, 232, 202, 195, 250, 198, 248, 186, 57, 94, 235, 229,
    254, 245, 93, 1, 62, 227, 204, 71, 43, 147, 221, 8, 158, 14, 5, 116, 142,
    148, 108>>,
  resp_mac_key: <<219, 214, 155, 162, 213, 145, 184, 147, 13, 47, 119, 42, 37,
    19, 73, 98, 40, 4, 103, 148, 217, 13, 129, 200, 89, 31, 185, 205, 71, 11,
    158, 209>>,
  resp_sym_key: <<51, 157, 177, 69, 93, 91, 61, 237, 134, 219, 38, 163, 250,
    237, 153, 100, 19, 144, 116, 44, 216, 243, 174, 236, 146, 180, 123, 46, 47,
    244, 163, 96>>,
  sha_alg: :sha256,
  sym_alg: :aes256,
  time_offset: 0,
  type: :user,
  url: "http://localhost:8082"
}

client ∴ [
      "That depends. Do you see me?",
      "If the rule you followed brought you to this, of what use was the rule?",
      "I know where you are.",
      "I won't tell you you can save yourself, because you can't.",
      "Would you hold still, please, sir?",
      "That's foolish. You pick the one right tool."
    ] |> Enum.map(fn quote ->
      {{:ok, reversed}, _} = anton |> SrpcClient.post("/reverse", quote)
      reversed
    end)
["?em ees uoy oD .sdneped tahT",
 "?elur eht saw esu tahw fo ,siht ot uoy thguorb dewollof uoy elur eht fI",
 ".era uoy erehw wonk I",
 ".t'nac uoy esuaceb ,flesruoy evas nac uoy uoy llet t'now I",
 "?ris ,esaelp ,llits dloh uoy dluoW",
 ".loot thgir eno eht kcip uoY .hsiloof s'tahT"]

client ∴ anton |> SrpcClient.info(:full)
%SrpcClient.Conn{
  accessed: -576460709,
  conn_id: "dMrbdnf7QDdBm6BGFrD84Nf4jQ",
  created: -576460744,
  crypt_count: 2,
  entity_id: "chigurh",
  keyed: -576460709,
  name: :UserConnection_1,
  req_mac_key: <<59, 44, 58, 80, 37, 69, 191, 25, 152, 73, 219, 119, 194, 7,
    125, 70, 171, 48, 110, 125, 151, 240, 65, 116, 162, 122, 150, 60, 181, 147,
    194, 144>>,
  req_sym_key: <<247, 179, 173, 180, 137, 105, 193, 242, 216, 132, 217, 143,
    247, 73, 244, 10, 185, 17, 233, 139, 92, 212, 151, 181, 126, 10, 70, 147,
    218, 128, 113, 133>>,
  resp_mac_key: <<95, 245, 226, 217, 21, 199, 13, 241, 186, 97, 249, 250, 201,
    140, 70, 232, 173, 215, 244, 84, 197, 203, 135, 43, 0, 90, 9, 103, 35, 61,
    39, 175>>,
  resp_sym_key: <<192, 92, 111, 31, 176, 92, 206, 170, 214, 122, 53, 160, 48, 0,
    39, 90, 96, 57, 134, 227, 235, 134, 174, 168, 34, 93, 19, 110, 34, 89, 10,
    79>>,
  sha_alg: :sha256,
  sym_alg: :aes256,
  time_offset: 0,
  type: :user,
  url: "http://localhost:8082"
}

Comparing the before and after information for the anton connection we see the keys are refreshed and the crypt_count is 2. That's because we sent 6 messages with a key_limit of 4, so after the 4th message the keys were refreshed before sending the last 2 messages.

It is also possible to configure SrpcClient to refresh the keys based on time of use:

config :srpc_client,
  srpc_file: "priv/client.srpc",
  transport: SrpcPoison,
  key_limit: 4,
  key_refresh: 60

The above configuration would limit the keys to a maximum of 4 uses, as well as to being used for no more than 60 seconds. These configuration settings can be used together or individually. Both take effect before each message is sent, i.e., the above configuration would trigger key refresh on the 5th message within 60 seconds, or on the Nth message (N < 5) after 60 seconds.

[Table of Contents](#TOC)
Reconnect

As we've seen, we can create either an SRPC lib or user connection between the client and the server. We've also seen that both the client and the server maintain information regarding this connection. For obvious reasons, the server can't keep client connection information indefinitely. The SrpcWorld.Server is configured (in the kncache configuration section) to keep each client's connection information for 3600 seconds. The server updates the client connection access time whenever it receives a message from the client, so the 3600 seconds actually represents an inactivity time after which the server will purge the connection information if no messages have been received from the client.

We can mimic the server purging a client connection by using the SrpcHandler configured for the server. First we'll create a connection and get the resulting conn_id:

client ∴ {:ok, conn} = SrpcClient.connect()
{:ok, #PID<0.279.0>}

client ∴ conn |> SrpcClient.info(:full) |> Map.get(:conn_id)
"PtMq76Nmth6bDDrnhBbq4mtmGM"

Then on the server, we'll purge that connection:

server ∴ SrpcWorld.Server.SrpcHandler.delete_conn("PtMq76Nmth6bDDrnhBbq4mtmGM")
:ok

Now, back on the client, we'll attempt to reverse a string with the now defunct connection:

client ∴ conn |> SrpcClient.post("/reverse", "string")
{{:invalid, "Stale connection"}, #PID<0.279.0>}

The client could check its own maintained access time for its connection before each use and create a new connection if necessary, but that would require the client know the connection retention time of the server. Furthermore, in the event of server restart, any existing client connections will be defunct.

SrpcClient provides a means to auto-reconnect a stale connection on-the-fly. In the :srpc_client configuration we'll add a reconnect option:

config :srpc_client,
  srpc_file: "priv/client.srpc",
  transport: SrpcPoison,
  key_limit: 4,
  key_refresh: 60,
  reconnect: true

With reconnect set to true, the first time the SRPC framework detects we are using a stale client it will attempt to reconnect. To see this in action, we'll need to restart the client (Cntl-C twice to stop the IEx process) to pick up the above configuration change.

On the client:

client ∴ {:ok, conn} = SrpcClient.connect()
{:ok, #PID<0.269.0>}

client ∴ conn |> SrpcClient.info(:full) |> Map.get(:conn_id)
"2d3fNt4g4H7MNr7JBfLLbR29hp"

On the server:

server ∴ SrpcWorld.Server.SrpcHandler.delete_conn("2d3fNt4g4H7MNr7JBfLLbR29hp")
:ok

And finally, on the client:

client ∴ {{:ok, result}, conn} = conn |> SrpcClient.post("/reverse", "string")
{{:ok, "gnirts"}, #PID<0.273.0>}

Notice we changed the code to rebind conn by pattern matching the return from the SrpcClient.post/3 call. If an auto-reconnect occurs, the returned pid will be for the new connection and the old connection will be discarded. We can see in the interactions above the original conn pid #PID<0.269.0> was updated to #PID<0.273.0>. If no reconnection is required (i.e., the conn is still valid on the server), the returned pid will be the same as that passed to SrpcClient.

Fin

That's it for the SrpcWorld demo of the SRPC framework. Please feel free to send questions or comments to Paul at paul@knoxen.com.

About

Elixir demo for SRPC security framework

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages