Skip to content

Commit

Permalink
Add more protection to transit encryption with expiry set to 60s
Browse files Browse the repository at this point in the history
Add decryption logic in multiple languages
  • Loading branch information
dormant-user committed Sep 19, 2024
1 parent 6b62751 commit 3a7af40
Show file tree
Hide file tree
Showing 8 changed files with 241 additions and 9 deletions.
33 changes: 33 additions & 0 deletions decoders/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
## Transit decryption in various languages


### Python

**Install requirements**
```shell
pip install requests cryptography
```

**Run decoder**
```shell
python decoder.py
```

### Go lang

**Run decoder**
```shell
go run decoder.go
```

### JavaScript

**Install requirements**
```shell
npm install axios
```

**Run decoder**
```shell
node decoder.js
```
102 changes: 102 additions & 0 deletions decoders/decoder.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
package main

import (
"crypto/aes"
"crypto/cipher"
"crypto/sha256"
"encoding/base64"
"encoding/json"
"fmt"
"net/http"
"os"
"time"
)

var apiKey = os.Getenv("APIKEY")

func transitDecrypt(ciphertext string, keyLength int) (map[string]interface{}, error) {
epoch := time.Now().Unix() / 60
hash := sha256.New()
hash.Write([]byte(fmt.Sprintf("%d.%s", epoch, apiKey)))
aesKey := hash.Sum(nil)[:keyLength]

cipherBytes, err := base64.StdEncoding.DecodeString(ciphertext)
if err != nil {
return nil, err
}

nonce := cipherBytes[:12]
cipherText := cipherBytes[12:]

block, err := aes.NewCipher(aesKey)
if err != nil {
return nil, err
}

gcm, err := cipher.NewGCM(block)
if err != nil {
return nil, err
}

decrypted, err := gcm.Open(nil, nonce, cipherText, nil)
if err != nil {
return nil, err
}

var result map[string]interface{}
err = json.Unmarshal(decrypted, &result)
if err != nil {
return nil, err
}

return result, nil
}

func getCipher() (string, error) {
req, err := http.NewRequest("GET", "http://0.0.0.0:8080/get-table", nil)
if err != nil {
return "", err
}

req.Header.Set("Accept", "application/json")
req.Header.Set("Authorization", "Bearer "+apiKey)
q := req.URL.Query()
q.Add("table_name", "default")
req.URL.RawQuery = q.Encode()

client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()

if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("HTTP error: %s", resp.Status)
}

var response struct {
Detail string `json:"detail"`
}
if err := json.NewDecoder(resp.Body).Decode(&response); err != nil {
return "", err
}

return response.Detail, nil
}

func main() {
ciphertext, err := getCipher()
if err != nil {
fmt.Println("Error getting cipher:", err)
return
}

decryptedData, err := transitDecrypt(ciphertext, 32)
if err != nil {
fmt.Println("Error decrypting:", err)
return
}

fmt.Println("Decrypted data:", decryptedData)
}
59 changes: 59 additions & 0 deletions decoders/decoder.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
const crypto = require('crypto');
const axios = require('axios');

const APIKEY = process.env.APIKEY;

async function transitDecrypt(ciphertext, keyLength = 32) {
const epoch = Math.floor(Date.now() / 60000);
const hash = crypto.createHash('sha256');
hash.update(`${epoch}.${APIKEY}`);
const aesKey = hash.digest().slice(0, keyLength);

const bufferCiphertext = Buffer.from(ciphertext, 'base64');
if (bufferCiphertext.length < 12 + 16) {
throw new Error('Ciphertext is too short');
}

const iv = bufferCiphertext.slice(0, 12); // First 12 bytes
const authTag = bufferCiphertext.slice(bufferCiphertext.length - 16); // Last 16 bytes
const encryptedData = bufferCiphertext.slice(12, bufferCiphertext.length - 16); // Data in between

const decipher = crypto.createDecipheriv('aes-256-gcm', aesKey, iv);
decipher.setAuthTag(authTag); // Set the authentication tag

let decrypted;
try {
decrypted = Buffer.concat([decipher.update(encryptedData), decipher.final()]);
} catch (err) {
throw new Error('Decryption failed: ' + err.message);
}

return JSON.parse(decrypted.toString());
}

async function getCipher() {
const headers = {
'accept': 'application/json',
'Authorization': `Bearer ${APIKEY}`,
};
const params = {
table_name: 'default',
};
const response = await axios.get('http://0.0.0.0:8080/get-table', {params, headers});
if (response.status !== 200) {
throw new Error(response.data);
}
return response.data.detail;
}

async function main() {
try {
const ciphertext = await getCipher();
const result = await transitDecrypt(ciphertext);
console.log(result);
} catch (error) {
console.error(error);
}
}

main();
37 changes: 37 additions & 0 deletions decoders/decoder.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import base64
import hashlib
import json
import os
import time
from typing import Dict, ByteString, Any

import requests
from cryptography.hazmat.primitives.ciphers.aead import AESGCM

APIKEY = os.environ["APIKEY"]


def transit_decrypt(ciphertext: str | ByteString, key_length: int = 32) -> Dict[str, Any]:
epoch = int(time.time()) // 60
hash_object = hashlib.sha256(f"{epoch}.{APIKEY}".encode())
aes_key = hash_object.digest()[:key_length]
if isinstance(ciphertext, str):
ciphertext = base64.b64decode(ciphertext)
decrypted = AESGCM(aes_key).decrypt(ciphertext[:12], ciphertext[12:], b"")
return json.loads(decrypted)


def get_cipher():
headers = {
'accept': 'application/json',
'Authorization': f'Bearer {APIKEY}',
}
params = {
'table_name': 'default',
}
response = requests.get('http://0.0.0.0:8080/get-table', params=params, headers=headers)
assert response.ok, response.text
return response.json()['detail']


print(transit_decrypt(ciphertext=get_cipher()))
3 changes: 0 additions & 3 deletions vaultapi/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,6 @@ def __init__(**kwargs) -> None:
"""Instantiates the env, session and database connections."""
models.env = squire.load_env(**kwargs)
models.session.fernet = Fernet(models.env.secret)
models.session.aes_key = transit.string_to_aes_key(
input_string=models.env.apikey, key_length=models.env.transit_key_length
)
models.database = models.Database(models.env.database)
default_allowed = ("0.0.0.0", "127.0.0.1", "localhost")
if models.env.host in default_allowed:
Expand Down
1 change: 0 additions & 1 deletion vaultapi/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,6 @@ class Session(BaseModel):
"""

fernet: Fernet | None = None
aes_key: ByteString | None = None
info: Dict[str, str] = {}
rps: Dict[str, int] = {}
allowed_origins: Set[str] = set()
Expand Down
11 changes: 7 additions & 4 deletions vaultapi/transit.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import hashlib
import json
import secrets
import time
from typing import Any, ByteString, Dict

from cryptography.hazmat.primitives.ciphers.aead import AESGCM
Expand Down Expand Up @@ -48,9 +49,9 @@ def encrypt(payload: Dict[str, Any], url_safe: bool = True) -> ByteString | str:
"""
nonce = secrets.token_bytes(12)
encoded = json.dumps(payload).encode()
# todo: instead of loading aes_key in memory,
# generate it on-demand and append a UTC time value that remains constant for at least 60s (probably env var)
ciphertext = nonce + AESGCM(models.session.aes_key).encrypt(nonce, encoded, b"")
epoch = int(time.time()) // 60
aes_key = string_to_aes_key(f"{epoch}.{models.env.apikey}", models.env.transit_key_length)
ciphertext = nonce + AESGCM(aes_key).encrypt(nonce, encoded, b"")
if url_safe:
return base64.b64encode(ciphertext).decode("utf-8")
return ciphertext
Expand All @@ -68,7 +69,9 @@ def decrypt(ciphertext: ByteString | str) -> Dict[str, Any]:
"""
if isinstance(ciphertext, str):
ciphertext = base64.b64decode(ciphertext)
decrypted = AESGCM(models.session.aes_key).decrypt(
epoch = int(time.time()) // 60
aes_key = string_to_aes_key(f"{epoch}.{models.env.apikey}", models.env.transit_key_length)
decrypted = AESGCM(aes_key).decrypt(
ciphertext[:12], ciphertext[12:], b""
)
return json.loads(decrypted)
Expand Down
4 changes: 3 additions & 1 deletion vaultapi/util.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import time
import base64
import hashlib
import importlib
Expand Down Expand Up @@ -73,7 +74,8 @@ def transit_decrypt(
Dict[str, Any]:
Returns the decrypted payload.
"""
hash_object = hashlib.sha256(apikey.encode())
epoch = int(time.time()) // 60
hash_object = hashlib.sha256(f"{epoch}.{apikey}".encode())
aes_key = hash_object.digest()[:key_length]
if isinstance(ciphertext, str):
ciphertext = base64.b64decode(ciphertext)
Expand Down

0 comments on commit 3a7af40

Please sign in to comment.