Skip to content

Commit

Permalink
Add hindi #119 + allow recordings encryption + decryption tooling.
Browse files Browse the repository at this point in the history
  • Loading branch information
cedricve committed Oct 23, 2023
1 parent f29b952 commit e474a62
Show file tree
Hide file tree
Showing 22 changed files with 393 additions and 62 deletions.
17 changes: 16 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,20 @@ The default username and password for the Kerberos Agent is:

**_Please note that you change the username and password for a final installation, see [Configure with environment variables](#configure-with-environment-variables) below._**

## Encryption

You can encrypt your recordings and outgoing MQTT messages with your own AES and RSA keys by enabling the encryption settings. Once enabled all your recordings will be encrypted using AES-256-CBC and your symmetric key. You can either use the default `openssl` toolchain to decrypt the recordings with your AES key, as following:

openssl aes-256-cbc -d -md md5 -in encrypted.mp4 -out decrypted.mp4 -k your-key-96ab185xxxxxxxcxxxxxxxx6a59c62e8

, and additionally you can decrypt a folder of recordings, using the Kerberos Agent binary as following:

go run main.go -action decrypt ./data/recordings your-key-96ab185xxxxxxxcxxxxxxxx6a59c62e8

or for a single file:

go run main.go -action decrypt ./data/recordings/video.mp4 your-key-96ab185xxxxxxxcxxxxxxxx6a59c62e8

## Configure and persist with volume mounts

An example of how to mount a host directory is shown below using `docker`, but is applicable for [all the deployment models and tools described above](#running-and-automating-a-kerberos-agent).
Expand Down Expand Up @@ -227,7 +241,8 @@ Next to attaching the configuration file, it is also possible to override the co
| `AGENT_KERBEROSVAULT_DIRECTORY` | The directory, in the provider, where the recordings will be stored in. | "" |
| `AGENT_DROPBOX_ACCESS_TOKEN` | The Access Token from your Dropbox app, that is used to leverage the Dropbox SDK. | "" |
| `AGENT_DROPBOX_DIRECTORY` | The directory, in the provider, where the recordings will be stored in. | "" |
| `AGENT_ENCRYPTION` | Enable 'true' or disable 'false' end-to-end encryption through MQTT (recordings will follow). | "false" |
| `AGENT_ENCRYPTION` | Enable 'true' or disable 'false' end-to-end encryption for MQTT messages. | "false" |
| `AGENT_ENCRYPTION_RECORDINGS` | Enable 'true' or disable 'false' end-to-end encryption for recordings. | "false" |
| `AGENT_ENCRYPTION_FINGERPRINT` | The fingerprint of the keypair (public/private keys), so you know which one to use. | "" |
| `AGENT_ENCRYPTION_PRIVATE_KEY` | The private key (assymetric/RSA) to decryptand sign requests send over MQTT. | "" |
| `AGENT_ENCRYPTION_SYMMETRIC_KEY` | The symmetric key (AES) to encrypt and decrypt request send over MQTT. | "" |
Expand Down
15 changes: 15 additions & 0 deletions machinery/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,21 @@ func main() {
case "discover":
log.Log.Info(timeout)

case "decrypt":
log.Log.Info("Decrypting: " + flag.Arg(0) + " with key: " + flag.Arg(1))
symmetricKey := []byte(flag.Arg(1))

if symmetricKey == nil || len(symmetricKey) == 0 {
log.Log.Fatal("Main: symmetric key should not be empty")
return
}
if len(symmetricKey) != 32 {
log.Log.Fatal("Main: symmetric key should be 32 bytes")
return
}

utils.Decrypt(flag.Arg(0), symmetricKey)

case "run":
{
// Print Kerberos.io ASCII art
Expand Down
22 changes: 22 additions & 0 deletions machinery/src/capture/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"time"

"github.com/gin-gonic/gin"
"github.com/kerberos-io/agent/machinery/src/encryption"
"github.com/kerberos-io/agent/machinery/src/log"
"github.com/kerberos-io/agent/machinery/src/models"
"github.com/kerberos-io/agent/machinery/src/utils"
Expand Down Expand Up @@ -405,6 +406,27 @@ func HandleRecordStream(queue *pubsub.Queue, configDirectory string, configurati
utils.CreateFragmentedMP4(fullName, config.Capture.FragmentedDuration)
}

// Check if we need to encrypt the recording.
if config.Encryption != nil && config.Encryption.Enabled == "true" && config.Encryption.Recordings == "true" && config.Encryption.SymmetricKey != "" {
// reopen file into memory 'fullName'
contents, err := os.ReadFile(fullName)
if err == nil {
// encrypt
encryptedContents, err := encryption.AesEncrypt(contents, config.Encryption.SymmetricKey)
if err == nil {
// write back to file
err := os.WriteFile(fullName, []byte(encryptedContents), 0644)
if err != nil {
log.Log.Error("HandleRecordStream: error writing file: " + err.Error())
}
} else {
log.Log.Error("HandleRecordStream: error encrypting file: " + err.Error())
}
} else {
log.Log.Error("HandleRecordStream: error reading file: " + err.Error())
}
}

// Create a symbol linc.
fc, _ := os.Create(configDirectory + "/data/cloud/" + name)
fc.Close()
Expand Down
16 changes: 11 additions & 5 deletions machinery/src/config/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -464,11 +464,10 @@ func OverrideWithEnvironmentVariables(configuration *models.Configuration) {

/* When encryption is enabled */
case "AGENT_ENCRYPTION":
if value == "true" {
configuration.Config.Encryption.Enabled = true
} else {
configuration.Config.Encryption.Enabled = false
}
configuration.Config.Encryption.Enabled = value
break
case "AGENT_ENCRYPTION_RECORDINGS":
configuration.Config.Encryption.Recordings = value
break
case "AGENT_ENCRYPTION_FINGERPRINT":
configuration.Config.Encryption.Fingerprint = value
Expand Down Expand Up @@ -510,6 +509,13 @@ func SaveConfig(configDirectory string, config models.Config, configuration *mod
}

func StoreConfig(configDirectory string, config models.Config) error {

// Encryption key can be set wrong.
encryptionPrivateKey := config.Encryption.PrivateKey
// Replace \\n by \n
encryptionPrivateKey = strings.ReplaceAll(encryptionPrivateKey, "\\n", "\n")
config.Encryption.PrivateKey = encryptionPrivateKey

// Save into database
if os.Getenv("DEPLOYMENT") == "factory" || os.Getenv("MACHINERY_ENVIRONMENT") == "kubernetes" {
// Write to mongodb
Expand Down
42 changes: 21 additions & 21 deletions machinery/src/encryption/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,58 +38,58 @@ func SignWithPrivateKey(data []byte, privateKey *rsa.PrivateKey) ([]byte, error)
return signature, err
}

func AesEncrypt(content string, password string) (string, error) {
func AesEncrypt(content []byte, password string) ([]byte, error) {
salt := make([]byte, 8)
_, err := rand.Read(salt)
if err != nil {
return "", err
return nil, err
}
key, iv, err := DefaultEvpKDF([]byte(password), salt)

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

mode := cipher.NewCBCEncrypter(block, iv)
cipherBytes := PKCS5Padding([]byte(content), aes.BlockSize)
cipherBytes := PKCS5Padding(content, aes.BlockSize)
mode.CryptBlocks(cipherBytes, cipherBytes)

data := make([]byte, 16+len(cipherBytes))
copy(data[:8], []byte("Salted__"))
copy(data[8:16], salt)
copy(data[16:], cipherBytes)
cipherText := make([]byte, 16+len(cipherBytes))
copy(cipherText[:8], []byte("Salted__"))
copy(cipherText[8:16], salt)
copy(cipherText[16:], cipherBytes)

cipherText := base64.StdEncoding.EncodeToString(data)
//cipherText := base64.StdEncoding.EncodeToString(data)
return cipherText, nil
}

func AesDecrypt(cipherText string, password string) (string, error) {
data, err := base64.StdEncoding.DecodeString(cipherText)
if err != nil {
return "", err
}
if string(data[:8]) != "Salted__" {
return "", errors.New("invalid crypto js aes encryption")
func AesDecrypt(cipherText []byte, password string) ([]byte, error) {
//data, err := base64.StdEncoding.DecodeString(cipherText)
//if err != nil {
// return nil, err
//}
if string(cipherText[:8]) != "Salted__" {
return nil, errors.New("invalid crypto js aes encryption")
}

salt := data[8:16]
cipherBytes := data[16:]
salt := cipherText[8:16]
cipherBytes := cipherText[16:]
key, iv, err := DefaultEvpKDF([]byte(password), salt)
if err != nil {
return "", err
return nil, err
}

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

mode := cipher.NewCBCDecrypter(block, iv)
mode.CryptBlocks(cipherBytes, cipherBytes)

result := PKCS5UnPadding(cipherBytes)
return string(result), nil
return result, nil
}

// https://stackoverflow.com/questions/27677236/encryption-in-javascript-and-decryption-with-php/27678978#27678978
Expand Down
5 changes: 3 additions & 2 deletions machinery/src/models/Config.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ type Config struct {
HubPrivateKey string `json:"hub_private_key" bson:"hub_private_key"`
HubSite string `json:"hub_site" bson:"hub_site"`
ConditionURI string `json:"condition_uri" bson:"condition_uri"`
Encryption *Encryption `json:"encryption" bson:"encryption"`
Encryption *Encryption `json:"encryption,omitempty" bson:"encryption",omitempty`
}

// Capture defines which camera type (Id) you are using (IP, USB or Raspberry Pi camera),
Expand Down Expand Up @@ -161,7 +161,8 @@ type Dropbox struct {

// Encryption
type Encryption struct {
Enabled bool `json:"enabled" bson:"enabled"`
Enabled string `json:"enabled" bson:"enabled"`
Recordings string `json:"recordings" bson:"recordings"`
Fingerprint string `json:"fingerprint" bson:"fingerprint"`
PrivateKey string `json:"private_key" bson:"private_key"`
SymmetricKey string `json:"symmetric_key" bson:"symmetric_key"`
Expand Down
24 changes: 14 additions & 10 deletions machinery/src/models/MQTT.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ func PackageMQTTMessage(configuration *Configuration, msg Message) ([]byte, erro
// At the moment we don't do the encryption part, but we'll implement it
// once the legacy methods (subscriptions are moved).
msg.Encrypted = false
if configuration.Config.Encryption != nil && configuration.Config.Encryption.Enabled {
if configuration.Config.Encryption != nil && configuration.Config.Encryption.Enabled == "true" {
msg.Encrypted = true
}
msg.PublicKey = ""
Expand Down Expand Up @@ -65,15 +65,19 @@ func PackageMQTTMessage(configuration *Configuration, msg Message) ([]byte, erro

// Create a 16bit key random
k := configuration.Config.Encryption.SymmetricKey
encryptedValue, err := encryption.AesEncrypt(string(data), k)

// Sign the encrypted value
signature, err := encryption.SignWithPrivateKey([]byte(encryptedValue), rsaKey)
base64Signature := base64.StdEncoding.EncodeToString(signature)

msg.Payload.EncryptedValue = encryptedValue
msg.Payload.Signature = base64Signature
msg.Payload.Value = make(map[string]interface{})
encryptedValue, err := encryption.AesEncrypt(data, k)
if err == nil {

data := base64.StdEncoding.EncodeToString(encryptedValue)
// Sign the encrypted value
signature, err := encryption.SignWithPrivateKey([]byte(data), rsaKey)
if err == nil {
base64Signature := base64.StdEncoding.EncodeToString(signature)
msg.Payload.EncryptedValue = data
msg.Payload.Signature = base64Signature
msg.Payload.Value = make(map[string]interface{})
}
}
}
}

Expand Down
23 changes: 19 additions & 4 deletions machinery/src/routers/mqtt/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package mqtt
import (
"crypto/rsa"
"crypto/x509"
"encoding/base64"
"encoding/json"
"encoding/pem"
"fmt"
Expand Down Expand Up @@ -168,7 +169,7 @@ func MQTTListenerHandler(mqttClient mqtt.Client, hubKey string, configDirectory
// Messages might be encrypted, if so we'll
// need to decrypt them.
var payload models.Payload
if message.Encrypted && configuration.Config.Encryption != nil && configuration.Config.Encryption.Enabled {
if message.Encrypted && configuration.Config.Encryption != nil && configuration.Config.Encryption.Enabled == "true" {
encryptedValue := message.Payload.EncryptedValue
if len(encryptedValue) > 0 {
symmetricKey := configuration.Config.Encryption.SymmetricKey
Expand Down Expand Up @@ -198,12 +199,16 @@ func MQTTListenerHandler(mqttClient mqtt.Client, hubKey string, configDirectory
if decryptedKey != nil {
if string(decryptedKey) == symmetricKey {
// Decrypt value with decryptedKey
decryptedValue, err := encryption.AesDecrypt(encryptedValue, string(decryptedKey))
data, err := base64.StdEncoding.DecodeString(encryptedValue)
if err != nil {
return
}
decryptedValue, err := encryption.AesDecrypt(data, string(decryptedKey))
if err != nil {
log.Log.Error("MQTTListenerHandler: error decrypting message: " + err.Error())
return
}
json.Unmarshal([]byte(decryptedValue), &payload)
json.Unmarshal(decryptedValue, &payload)
} else {
log.Log.Error("MQTTListenerHandler: error decrypting message, assymetric keys do not match.")
return
Expand Down Expand Up @@ -333,10 +338,16 @@ func HandleRequestConfig(mqttClient mqtt.Client, hubKey string, payload models.P

if key != "" && name != "" {

// Copy the config, as we don't want to share the encryption part.
deepCopy := configuration.Config

var configMap map[string]interface{}
inrec, _ := json.Marshal(configuration.Config)
inrec, _ := json.Marshal(deepCopy)
json.Unmarshal(inrec, &configMap)

// Unset encryption part.
delete(configMap, "encryption")

message := models.Message{
Payload: models.Payload{
Action: "receive-config",
Expand Down Expand Up @@ -370,6 +381,10 @@ func HandleUpdateConfig(mqttClient mqtt.Client, hubKey string, payload models.Pa
if configPayload.Timestamp != 0 {

config := configPayload.Config

// Make sure to remove Encryption part, as we don't want to save it.
config.Encryption = configuration.Config.Encryption

err := configService.SaveConfig(configDirectory, config, configuration, communication)
if err == nil {
log.Log.Info("HandleUpdateConfig: Config updated")
Expand Down
65 changes: 65 additions & 0 deletions machinery/src/utils/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
"strings"
"time"

"github.com/kerberos-io/agent/machinery/src/encryption"
"github.com/kerberos-io/agent/machinery/src/log"
"github.com/kerberos-io/agent/machinery/src/models"
)
Expand Down Expand Up @@ -330,3 +331,67 @@ func PrintConfiguration(configuration *models.Configuration) {
}
log.Log.Info("Printing our configuration (config.json): " + configurationVariables)
}

func Decrypt(directoryOrFile string, symmetricKey []byte) {
// Check if file or directory
fileInfo, err := os.Stat(directoryOrFile)
if err != nil {
log.Log.Fatal(err.Error())
return
}

var files []string
if fileInfo.IsDir() {
// Create decrypted directory
err = os.MkdirAll(directoryOrFile+"/decrypted", 0755)
if err != nil {
log.Log.Fatal(err.Error())
return
}
dir, err := os.ReadDir(directoryOrFile)
if err != nil {
log.Log.Fatal(err.Error())
return
}
for _, file := range dir {
// Check if file is not a directory
if !file.IsDir() {
// Check if an mp4 file
if strings.HasSuffix(file.Name(), ".mp4") {
files = append(files, directoryOrFile+"/"+file.Name())
}
}
}
} else {
files = append(files, directoryOrFile)
}

// We'll loop over all files and decrypt them one by one.
for _, file := range files {

// Read file
content, err := os.ReadFile(file)
if err != nil {
log.Log.Fatal(err.Error())
return
}
// Decrypt using AES key
decrypted, err := encryption.AesDecrypt(content, string(symmetricKey))
if err != nil {
log.Log.Fatal("Something went wrong while decrypting: " + err.Error())
return
}

// Write decrypted content to file with appended .decrypted
// Get filename split by / and get last element.
fileParts := strings.Split(file, "/")
fileName := fileParts[len(fileParts)-1]
pathToFile := strings.Join(fileParts[:len(fileParts)-1], "/")

err = os.WriteFile(pathToFile+"/decrypted/"+fileName, []byte(decrypted), 0644)
if err != nil {
log.Log.Fatal(err.Error())
return
}
}
}
Loading

0 comments on commit e474a62

Please sign in to comment.