/opsep-server

Separate your data from your keys and add a rate-limiter.

Primary LanguageGoOtherNOASSERTION

Op-Sep: Separate Your Data from your Keys

Use asymmetric cryptography to enforce rate-limits on your most sensitive data. Operationally separate your data from your keys. Reduce the damage of data-breaches 100x, and maintain an audit log to track exposure.

Setup

Fetch Repo

$ git clone git@github.com:opsep/opsep-server.git && cd opsep-server

Create an RSA Keypair

$ openssl genrsa -out pem.priv 4096 && openssl rsa -in pem.priv -pubout -out crt.pub
Generating RSA private key, 4096 bit long modulus (2 primes)
...........................................++++
...............................++++
e is 65537 (0x010001)
writing RSA key

(this is how the keypairs in insecure_certs were generated)

Run the Server

$ RSA_PRIVATE_KEY="$(cat insecure_certs/pem.priv)" go run *.go

(substitute your own private key)

Confirm Server Online

This will also output the public configuration (your RSA private key is never extracted):

$ curl localhost:8080 
{
  "sqliteFilePath": "opsep.sqlite3",
  "serverHost": "localhost",
  "serverPort": "8080",
  "rsaPubKey": "-----BEGIN RSA PUBLIC KEY-----\nMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA7q4R3soRD2CrjL13OK6Y\nSBG8wpjP5sbfkL0QhpJMH87grlR2SS3CUnbYCOONzQiJ3OuKAViy/lMw1KsmG9Nn\nhAot2acg1iNyZRY33LR2jwmfFF+2iRp0itPQeOHY6GS8m3WLCMtC/kWUq0Bl5g1P\nYa9JXwSkTTRJunNH0TPk8uqwFeVhpT336M1H6ed105L8a8W3mpSwlwePron7pLf7\nwD32m9RT0nNdnHBDQCsUKS/Gdp+saLYWTgj0rpnQCe8f1p3g36Gm0gTzr3X0Adow\n8gIPfxO4HU/0cdL+Pw4mpcsWJ4531taRLLGb+a2la2zAUteYcS+8d4Nb8Omkbz39\nPylvKP6R1kHElqlF3BnwUp0AdcAvOLdeX8kYUlbKE8xwjHm/KwwleKlcAZDam7hC\nRw72JUQiod0E7My+SiZ3Ij5zKnxZXmAF5BX8T+YSqSzR4Qdp2QU9L9GgAZo/HPBN\nwME9v8usjEzrEItSSg3Nn10+J+ygsCqjrCT8CnSvD8wEyDSdO/Jly9DnWJ6B2HJE\nOc4wxWGFTCE0wiQOwC3IPNxFhuWun6/4tsEQcDs5XHaBXIHry5WCiVkjwa2pc95x\niXcfoQWr1A/jLe/MrZyN4yrgDK9mmQxxNzVfLj8S9NPjJMv+K7BKvtOmvoqsf13K\n6hYJGkAdR0d99DNFlllRm7cCAwEAAQ==\n-----END RSA PUBLIC KEY-----\n",
  "decryptsAllowedPerPeriod": 100,
  "periodInSeconds": 600
}

Run Tests

See client info below.


Use

The proper way to use opsep is with a client library that abstracts away all these complexities. The information below is only for advanced users, for example those wishing to write their own client library (perhaps in a new langauge). You can check out the python opsep client here

Encryption

Encrypt a key locally (this would be randomly generated by your client library and not 0000...0000):

$ TO_DECRYPT=$(echo "{\"key\":\"00000000000000000000000000000000\"}" | openssl pkeyutl -encrypt -pubin -inkey insecure_certs/crt.pub -pkeyopt rsa_padding_mode:oaep -pkeyopt rsa_oaep_md:sha256 -pkeyopt rsa_mgf1_md:sha256 | base64)

You can inspect the value stored in TO_DECRYPT like this:

$ echo $TO_DECRYPT

Decryption

Make an API call to decrypt the file you just made (this is how your client library would later retrieve the randomly generated data-encryption-key it previously generated):

$ curl -X POST localhost:8080/api/v1/decrypt -H 'Content-Type: application/json' -d '{"key_retrieval_ciphertext":"'$(echo $TO_DECRYPT)'"}' | python -m json.tool
{
    "keyRecovered": "00000000000000000000000000000000",
    "requestSHA256": "1e16673d9b0bb33e7cfb84d6c0bf96970f3cfc34c4f7f41987bd624c0912f69a",
    "ratelimitTotal": 100,
    "ratelimitRemaining": 99,
    "ratelimitResetsIn": 599
}

Audit Logging for Decryption Requests

Note that when inspecting audit logs (individual records or bulk dumps), opsep doesn't store/dump sensitive data.

Query by Key Retrieval Ciphertext

Calculate the hash of your decryption instructions:

$ echo $TO_DECRYPT | base64 --decode | shasum -a 256
55a80d54fd68dea27f4186a9f6466082f02af25939546d974eb19c3eee4e4114  -

(your result will be different, as asymmetric encryption uses a randomly generated nonce each time it is run)

Query to see all past decryptions using those decryption instructions:

$ curl localhost:8080/api/v1/logs/55a80d54fd68dea27f4186a9f6466082f02af25939546d974eb19c3eee4e4114 | python -m json.tool
[
    {
        "ServerLogID": 18,
        "CreatedAt": "2020-08-11T18:10:09Z",
        "RequestSha256Digest": "55a80d54fd68dea27f4186a9f6466082f02af25939546d974eb19c3eee4e4114",
        "RequestIPAddress": "127.0.0.1",
        "RequestUserAgent": "curl/7.64.1",
        "ClientRecordID": null,
        "DeprecateAt": null,
        "RiskMultiplier": null
    }
]

There could be multiple instances of decrypting that data, as your client might request decryption multiple times.

Query by Client Record ID

Note that this is only possible if the client_record_id was originally encrypted as part of the key_retrieval_ciphertext.

$ sqlite3 opsep.sqlite3 -header -csv 'SELECT * FROM api_calls WHERE client_record_id = "aaaaaaaa-0000-bbbb-1111-cccccccccccc" LIMIT 2;'
id,created_at,request_sha256digest,request_ip_address,request_user_agent,response_dsha256digest,deprecate_at,client_record_id,risk_multiplier
3,"2020-08-11 17:42:54",c188a894fc1bc77ebd5872ec0f49f4d2f5876ea3aa7d6176258ea7d2fc1f0328,127.0.0.1,curl/7.64.1,9dd45edc9bf8afc0f06bd369da7e586169aaa2b0d616a3cdb9974344f7a5cab6,,aaaaaaaa-0000-bbbb-1111-cccccccccccc,
4,"2020-08-11 17:43:28",78d3ff5f0d6745f904959ef84a301024f6090e44309e6ab0ee195346e83d922e,127.0.0.1,curl/7.64.1,9dd45edc9bf8afc0f06bd369da7e586169aaa2b0d616a3cdb9974344f7a5cab6,,aaaaaaaa-0000-bbbb-1111-cccccccccccc,

Dump All Decryption Requests

Worried about a breach? See all decrypts as CSV:

$ sqlite3 opsep.sqlite3 -header -csv 'SELECT * FROM api_calls;'

Advanced Features

Key Deprecation

Pick an expiration date for your key when you generate it:

$ TO_DECRYPT=$(echo "{\"key\":\"00000000000000000000000000000000\", \"deprecate_at\":\"2020-01-01T12:00:00Z\"}" | openssl pkeyutl -encrypt -pubin -inkey insecure_certs/crt.pub -pkeyopt rsa_padding_mode:oaep -pkeyopt rsa_oaep_md:sha256 -pkeyopt rsa_mgf1_md:sha256 | base64)
$ curl -X POST localhost:8080/api/v1/decrypt -H 'Content-Type: application/json' -d '{"key_retrieval_ciphertext":"'$(echo $TO_DECRYPT)'"}' | python -m json.tool
{
    "error_name": "DeprecatedDecryptionKeyError",
    "error_description": "Key to decrypt this payload marked as deprecated."
}

Note that your data can still be recovered if you have your Key Encryption Key, but it is not defualt accessible via this service.

Risk Multiplier

Have specific records that you know are extra-sensitive? You can make these count as multiple records for the purposes of your rate-limit:

$ TO_DECRYPT=$(echo "{\"key\":\"00000000000000000000000000000000\", \"risk_multiplier\":10}" | openssl pkeyutl -encrypt -pubin -inkey insecure_certs/crt.pub -pkeyopt rsa_padding_mode:oaep -pkeyopt rsa_oaep_md:sha256 -pkeyopt rsa_mgf1_md:sha256 | base64)
$ curl -s -X POST localhost:8080/api/v1/decrypt -H 'Content-Type: application/json' -d '{"key_retrieval_ciphertext":"'$(echo $TO_DECRYPT)'"}' | python -m json.tool
{
    "keyRecovered": "00000000000000000000000000000000",
    "requestSHA256": "33dac6df324b177ce26720eed545f97e69c9bce5e0caa083e62a665996509cec",
    "ratelimitTotal": 100,
    "ratelimitRemaining": 90,
    "ratelimitResetsIn": 599
}

ratelimitRemaining correctly fell from 100 to 90 in one API decryption requests.

Client Record ID

Tracking decryption using key_retrieval_ciphertext can be cumbersome. Even easier, at the time of encryption you can save your local client record ID. Note that regardless of underlying type this must be saved as a string (normal for UUIDs, but counterintuitive for INTs).

$ TO_DECRYPT=$(echo "{\"key\":\"00000000000000000000000000000000\", \"client_record_id\":\"aaaaaaaa-0000-bbbb-1111-cccccccccccc\"}" | openssl pkeyutl -encrypt -pubin -inkey insecure_certs/crt.pub -pkeyopt rsa_padding_mode:oaep -pkeyopt rsa_oaep_md:sha256 -pkeyopt rsa_mgf1_md:sha256 | base64)
$ curl -s -X POST localhost:8080/api/v1/decrypt -H 'Content-Type: application/json' -d '{"key_retrieval_ciphertext":"'$(echo $TO_DECRYPT)'"}' | python -m json.tool
{
    "keyRecovered": "00000000000000000000000000000000",
    "requestSHA256": "6e7679934a8122b6f4e91d7a5da09e37dbc31777c714b23294eeb329a3a586cf",
    "ratelimitTotal": 100,
    "ratelimitRemaining": 99,
    "ratelimitResetsIn": 599
}

What's particularly powerful is that you can query your Opsep server by these IDs. See Audit Logging below.

Rate-Limit Test

If you want a rough test of 429-ing, you can do this:

$ for i in {1..99}; do curl [...] "http://localhost:8080/api/v1/decrypt" ; done

Load Testing

Run server with a high threshold of decryptions:

RSA_PRIVATE_KEY="$(cat insecure_certs/pem.priv)" DECRYPTS_PER_PERIOD=99999 go run *.go

Before and after your load test, query your sqlite DB to confirm the correct # of records were inserted:

$ sqlite3 opsep.sqlite3 'SELECT COUNT(1) from api_calls;'
32743

Using Curl

Make requests as client:

$ TO_DECRYPT=$(echo "{\"key\":\"00000000000000000000000000000000\"}" | openssl pkeyutl -encrypt -pubin -inkey insecure_certs/crt.pub -pkeyopt rsa_padding_mode:oaep -pkeyopt rsa_oaep_md:sha256 -pkeyopt rsa_mgf1_md:sha256 | base64)
$ time for i in {1..100}; do curl -s -o /dev/null -X POST localhost:8080/api/v1/decrypt -H 'Content-Type: application/json' -d '{"key_retrieval_ciphertext":"'$(echo $TO_DECRYPT)'"}' ; done
real	0m1.969s
user	0m0.325s
sys	0m0.495s

(On Windows replace /dev/null with nul)

Using Apache Bench

Download ab.

Note that you must use 127.0.0.1 instead of localhost.

$ TO_DECRYPT=$(echo "{\"key\":\"00000000000000000000000000000000\"}" | openssl pkeyutl -encrypt -pubin -inkey insecure_certs/crt.pub -pkeyopt rsa_padding_mode:oaep -pkeyopt rsa_oaep_md:sha256 -pkeyopt rsa_mgf1_md:sha256 | base64)
$ echo "{\"key_retrieval_ciphertext\":\"$( echo $TO_DECRYPT )\"}" > ab.json
$ ab -p ab.json -T "application/json" -c 2 -n 100 http://127.0.0.1:8080/api/v1/decrypt 
This is ApacheBench, Version 2.3 <$Revision: 1843412 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/

Benchmarking 127.0.0.1 (be patient).....done


Server Software:        
Server Hostname:        127.0.0.1
Server Port:            8080

Document Path:          /api/v1/decrypt
Document Length:        213 bytes

Concurrency Level:      2
Time taken for tests:   0.563 seconds
Complete requests:      100
Failed requests:        0
Total transferred:      33700 bytes
Total body sent:        86600
HTML transferred:       21300 bytes
Requests per second:    177.77 [#/sec] (mean)
Time per request:       11.250 [ms] (mean)
Time per request:       5.625 [ms] (mean, across all concurrent requests)
Transfer rate:          58.50 [Kbytes/sec] received
                        150.34 kb/s sent
                        208.85 kb/s total

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    0   0.1      0       1
Processing:     9   11   1.7     10      15
Waiting:        9   11   1.7     10      15
Total:          9   11   1.7     11      15

Percentage of the requests served within a certain time (ms)
  50%     11
  66%     12
  75%     12
  80%     13
  90%     14
  95%     14
  98%     15
  99%     15
 100%     15 (longest request)

One-Liner

Regular:

$ curl -s -X POST localhost:8080/api/v1/decrypt -H 'Content-Type: application/json' -d '{"key_retrieval_ciphertext":"'$(echo "{\"key\":\"00000000000000000000000000000000\"}" | openssl pkeyutl -encrypt -pubin -inkey insecure_certs/crt.pub -pkeyopt rsa_padding_mode:oaep -pkeyopt rsa_oaep_md:sha256 -pkeyopt rsa_mgf1_md:sha256 | base64)'"}' | python -m json.tool
{
    "keyRecovered": "00000000000000000000000000000000",
    "requestSHA256": "df9e875c1670896d1213f6fa401f12ce4bcfdc047d0ab050620f70626db53b89",
    "ratelimitTotal": 100,
    "ratelimitRemaining": 99,
    "ratelimitResetsIn": 599
}

Expired key:

$ curl -s -X POST localhost:8080/api/v1/decrypt -H 'Content-Type: application/json' -d '{"key_retrieval_ciphertext":"'$(echo "{\"key\":\"00000000000000000000000000000000\", \"deprecate_at\":\"2020-01-01T12:00:00Z\"}" | openssl pkeyutl -encrypt -pubin -inkey insecure_certs/crt.pub -pkeyopt rsa_padding_mode:oaep -pkeyopt rsa_oaep_md:sha256 -pkeyopt rsa_mgf1_md:sha256 | base64)'"}' | python -m json.tool
{
    "error_name": "DeprecatedDecryptionKeyError",
    "error_description": "Key to decrypt this payload marked as deprecated."
}

Fancy. In addition to key we include deprecate_at, client_record_id, and risk_multiplier):

$ curl -s -X POST localhost:8080/api/v1/decrypt -H 'Content-Type: application/json' -d '{"key_retrieval_ciphertext":"'$(echo "{\"key\":\"00000000000000000000000000000000\", \"deprecate_at\":\"2030-01-01T12:00:00Z\", \"client_record_id\":\"aaaaaaaa-0000-bbbb-1111-cccccccccccc\", \"risk_multiplier\":"3"}" | openssl pkeyutl -encrypt -pubin -inkey insecure_certs/crt.pub -pkeyopt rsa_padding_mode:oaep -pkeyopt rsa_oaep_md:sha256 -pkeyopt rsa_mgf1_md:sha256 | base64)'"}' | python -m json.tool
{
    "keyRecovered": "00000000000000000000000000000000",
    "requestSHA256": "e505f29ab3da7fa6e02ef7cc2ff42bcef66cb4e2fff814ae8d36344306a07a40",
    "ratelimitTotal": 100,
    "ratelimitRemaining": 99,
    "ratelimitResetsIn": 599
}

Extract Public Key from Opsep Server

To be sure that you're encrypting your data locally with the correct pubkey:

$ curl localhost:8080 | python3 -c "import sys, json; print(json.load(sys.stdin)['rsaPubKey'].strip())" | awk '{gsub(/\\n/,"\n")}1' > crt.pub

Advanced Deployment Options

Other environmental variable options include SQLITE_FILEPATH, SERVER_HOST, SERVER_PORT, DECRYPTS_PER_PERIOD, and PERIOD_IN_SECONDS. See config.go for more info.

HSM

RSA is compatible with all major HSMs. You're on your own for implementating that.

Help

File an issue or contact me at opsep@michaelflaxman.com if you're stuck.