diff --git a/machinery/src/capture/main.go b/machinery/src/capture/main.go index d6e71da..e6e3617 100644 --- a/machinery/src/capture/main.go +++ b/machinery/src/capture/main.go @@ -53,12 +53,13 @@ func publishRecordingState(mqttClient mqtt.Client, hubKey string, configuration } } -func recordingUploadMetadata(name, deviceKey string, timestamp int64, mp4Video *video.MP4) models.RecordingUploadMetadata { +func recordingUploadMetadata(name, deviceKey string, timestamp int64, mp4Video *video.MP4, encrypted bool) models.RecordingUploadMetadata { metadata := models.RecordingUploadMetadata{ FileName: filepath.Base(name), DeviceKey: deviceKey, Timestamp: timestamp, Duration: mp4Video.VideoTotalDuration, + Encrypted: encrypted, } value := mp4Video.AverageFPS() if value > 0 && value <= 240 && !math.IsInf(value, 0) && !math.IsNaN(value) { @@ -481,6 +482,7 @@ func HandleRecordStream(queue *packets.Queue, configDirectory string, configurat log.Info("capture.main.HandleRecordStream(continuous): no video data recorded, not renaming file.") } + encrypted := false // 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' @@ -493,6 +495,8 @@ func HandleRecordStream(queue *packets.Queue, configDirectory string, configurat err := os.WriteFile(fullName, []byte(encryptedContents), 0644) if err != nil { log.Error("capture.main.HandleRecordStream(continuous): error writing file: " + err.Error()) + } else { + encrypted = true } } else { log.Error("capture.main.HandleRecordStream(continuous): error encrypting file: " + err.Error()) @@ -502,7 +506,7 @@ func HandleRecordStream(queue *packets.Queue, configDirectory string, configurat } } - queueRecordingForUpload(configDirectory, recordingUploadMetadata(name, config.Key, startRecording, mp4Video)) + queueRecordingForUpload(configDirectory, recordingUploadMetadata(name, config.Key, startRecording, mp4Video, encrypted)) recordingStatus = "idle" @@ -638,6 +642,7 @@ func HandleRecordStream(queue *packets.Queue, configDirectory string, configurat log.Info("capture.main.HandleRecordStream(continuous): no video data recorded, not renaming file.") } + encrypted := false // 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' @@ -650,6 +655,8 @@ func HandleRecordStream(queue *packets.Queue, configDirectory string, configurat err := os.WriteFile(fullName, []byte(encryptedContents), 0644) if err != nil { log.Error("capture.main.HandleRecordStream(motiondetection): error writing file: " + err.Error()) + } else { + encrypted = true } } else { log.Error("capture.main.HandleRecordStream(motiondetection): error encrypting file: " + err.Error()) @@ -659,7 +666,7 @@ func HandleRecordStream(queue *packets.Queue, configDirectory string, configurat } } - queueRecordingForUpload(configDirectory, recordingUploadMetadata(name, config.Key, startRecording, mp4Video)) + queueRecordingForUpload(configDirectory, recordingUploadMetadata(name, config.Key, startRecording, mp4Video, encrypted)) recordingStatus = "idle" @@ -905,6 +912,7 @@ func HandleRecordStream(queue *packets.Queue, configDirectory string, configurat log.Info("capture.main.HandleRecordStream(motiondetection): no video data recorded, not renaming file.") } + encrypted := false // 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' @@ -917,6 +925,8 @@ func HandleRecordStream(queue *packets.Queue, configDirectory string, configurat err := os.WriteFile(fullName, []byte(encryptedContents), 0644) if err != nil { log.Error("capture.main.HandleRecordStream(motiondetection): error writing file: " + err.Error()) + } else { + encrypted = true } } else { log.Error("capture.main.HandleRecordStream(motiondetection): error encrypting file: " + err.Error()) @@ -926,7 +936,7 @@ func HandleRecordStream(queue *packets.Queue, configDirectory string, configurat } } - queueRecordingForUpload(configDirectory, recordingUploadMetadata(name, config.Key, displayTime, mp4Video)) + queueRecordingForUpload(configDirectory, recordingUploadMetadata(name, config.Key, displayTime, mp4Video, encrypted)) // Clean up the recording directory if necessary. CleanupRecordingDirectory(configDirectory, configuration) diff --git a/machinery/src/capture/main_test.go b/machinery/src/capture/main_test.go index e68c284..ea0dd82 100644 --- a/machinery/src/capture/main_test.go +++ b/machinery/src/capture/main_test.go @@ -42,7 +42,7 @@ func TestQueueRecordingForUploadStoresFinalizedMetadata(t *testing.T) { } mp4Video := &video.MP4{VideoTotalDuration: 20452, SampleCount: 613} - metadata := recordingUploadMetadata("recording.mp4", "device-key", 1785934709414, mp4Video) + metadata := recordingUploadMetadata("recording.mp4", "device-key", 1785934709414, mp4Video, true) queueRecordingForUpload(configDirectory, metadata) got, err := os.ReadFile(filepath.Join(configDirectory, "data", "cloud", "recording.metadata")) @@ -54,7 +54,7 @@ func TestQueueRecordingForUploadStoresFinalizedMetadata(t *testing.T) { t.Fatalf("decode upload marker: %v", err) } expectedFPS := mp4Video.AverageFPS() - if stored.FileName != "recording.mp4" || stored.DeviceKey != "device-key" || stored.Timestamp != 1785934709414 || stored.Duration != 20452 || math.Abs(stored.FPS-expectedFPS) > 1e-9 { + if stored.FileName != "recording.mp4" || stored.DeviceKey != "device-key" || stored.Timestamp != 1785934709414 || stored.Duration != 20452 || math.Abs(stored.FPS-expectedFPS) > 1e-9 || !stored.Encrypted { t.Fatalf("upload marker = %+v", stored) } if stored.FPS == math.Floor(stored.FPS) { diff --git a/machinery/src/cloud/recording_metadata.go b/machinery/src/cloud/recording_metadata.go index 48f4d63..3773637 100644 --- a/machinery/src/cloud/recording_metadata.go +++ b/machinery/src/cloud/recording_metadata.go @@ -15,6 +15,7 @@ import ( const recordingFPSHeader = "X-Kerberos-Storage-Fps" const recordingDurationHeader = "X-Kerberos-Storage-Duration" const recordingTimestampHeader = "X-Kerberos-Storage-Timestamp" +const recordingEncryptedHeader = "X-Kerberos-Storage-Encrypted" // queuedRecordingFPS reads the FPS snapshot written into the upload marker // when the recording was finalized. Historical empty markers intentionally @@ -84,5 +85,8 @@ func setQueuedRecordingMetadataHeaders(header http.Header, fileName string) { if metadata.Timestamp > 0 { header.Set(recordingTimestampHeader, strconv.FormatInt(metadata.Timestamp, 10)) } + if metadata.Encrypted { + header.Set(recordingEncryptedHeader, "true") + } } } diff --git a/machinery/src/cloud/tus_client.go b/machinery/src/cloud/tus_client.go index 4579ae4..2c2ded5 100644 --- a/machinery/src/cloud/tus_client.go +++ b/machinery/src/cloud/tus_client.go @@ -361,6 +361,9 @@ func addRecordingTusMetadata(values map[string]string, fileName string) { if metadata.Timestamp > 0 { values["timestamp"] = strconv.FormatInt(metadata.Timestamp, 10) } + if metadata.Encrypted { + values["encrypted"] = "true" + } } // tusCreate performs the tus "creation" request (POST). On success it returns diff --git a/machinery/src/cloud/tus_client_test.go b/machinery/src/cloud/tus_client_test.go index 68a42ca..99129f0 100644 --- a/machinery/src/cloud/tus_client_test.go +++ b/machinery/src/cloud/tus_client_test.go @@ -386,7 +386,7 @@ func TestQueuedRecordingFPSAllowsMissingHistoricalMarker(t *testing.T) { func TestQueuedRecordingMetadataHeaders(t *testing.T) { fileName := "recording.mp4" withRecording(t, fileName, []byte("recording")) - withQueuedRecordingFPS(t, fileName, `{"filename":"recording.mp4","device_key":"device-key","timestamp":1785934709414,"duration":20452,"fps":25}`) + withQueuedRecordingFPS(t, fileName, `{"filename":"recording.mp4","device_key":"device-key","timestamp":1785934709414,"duration":20452,"fps":25,"encrypted":true}`) header := make(http.Header) setQueuedRecordingMetadataHeaders(header, fileName) @@ -399,6 +399,14 @@ func TestQueuedRecordingMetadataHeaders(t *testing.T) { if got := header.Get(recordingTimestampHeader); got != "1785934709414" { t.Fatalf("timestamp header = %q", got) } + if got := header.Get(recordingEncryptedHeader); got != "true" { + t.Fatalf("encrypted header = %q", got) + } + metadata := map[string]string{} + addRecordingTusMetadata(metadata, fileName) + if got := metadata["encrypted"]; got != "true" { + t.Fatalf("encrypted TUS metadata = %q", got) + } } func TestQueuedRecordingFPSAllowsLegacyMarkerFileName(t *testing.T) { diff --git a/machinery/src/models/recording_upload_metadata.go b/machinery/src/models/recording_upload_metadata.go index 504134a..ac253d4 100644 --- a/machinery/src/models/recording_upload_metadata.go +++ b/machinery/src/models/recording_upload_metadata.go @@ -16,6 +16,7 @@ type RecordingUploadMetadata struct { Timestamp int64 `json:"timestamp"` // Unix milliseconds. Duration uint64 `json:"duration"` // Milliseconds. FPS float64 `json:"fps,omitempty"` + Encrypted bool `json:"encrypted,omitempty"` } // RecordingUploadMetadataFileName returns the queue marker name associated