Module: RobocapCenc::SDK::Mp4Cenc

Defined in:
lib/robocap/sdk/mp4_cenc.rb

Constant Summary collapse

CEKA_TAG =
'cenc_cek_wrapped_b64'
CUSTOMER_ID_TAG =
'cenc_customer_id'
CUSTOMER_ID_FALLBACK_TAG =
'deviceid'
HOST_TAG =
'host'
KID_TAG =
'cenc_kid_hex'
CENC_WRAPPED_ALGO_TAG =
'cenc_wrapped_algo'
CENC_STRIP_TAGS_ON_DECRYPT =
[
  CEKA_TAG,
  CENC_WRAPPED_ALGO_TAG,
].freeze

Class Method Summary collapse

Class Method Details

.detect_product_line(mp4_path) ⇒ Object

Product line is derived from the filename prefix, which decides whether the vault is keyed by the deviceid or the host tag.



86
87
88
89
90
91
# File 'lib/robocap/sdk/mp4_cenc.rb', line 86

def detect_product_line(mp4_path)
  stem = Pathname(mp4_path).basename('.*').to_s.downcase
  return PRODUCT_LINE_ROBOWRIST if stem.start_with?('robowrist_')
  return PRODUCT_LINE_ROBOCAP if stem.start_with?('robocap_')
  PRODUCT_LINE_LEGACY
end

.has_cenc_tags(mp4_path, ffprobe_executable: nil) ⇒ Object



144
145
146
147
148
149
150
# File 'lib/robocap/sdk/mp4_cenc.rb', line 144

def has_cenc_tags(mp4_path, ffprobe_executable: nil)
  path = Pathname(mp4_path).expand_path
  tags = read_format_tags(path, ffprobe_executable: ffprobe_executable)
  has_required_tags_for_product?(tags, detect_product_line(path))
rescue Error
  false
end

.load_cenc_metadata(mp4_path, ffprobe_executable: nil) ⇒ Object



136
137
138
139
140
141
142
# File 'lib/robocap/sdk/mp4_cenc.rb', line 136

def (mp4_path, ffprobe_executable: nil)
  path = Pathname(mp4_path).expand_path
  tags = read_format_tags(path, ffprobe_executable: ffprobe_executable)
  (
    tags, mp4_path: path, product_line: detect_product_line(path),
  )
end

.parse_cenc_metadata_from_tags(tags, mp4_path: nil, product_line: nil) ⇒ Object



93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
# File 'lib/robocap/sdk/mp4_cenc.rb', line 93

def (tags, mp4_path: nil, product_line: nil)
  unless tags[CEKA_TAG] && !tags[CEKA_TAG].empty?
    raise Error.new(
      code: ErrorCode::ERR_CENC_TAGS_MISSING,
      message: "Missing CENC tags: #{CEKA_TAG}",
    )
  end

  line = product_line
  line ||= mp4_path ? detect_product_line(mp4_path) : PRODUCT_LINE_LEGACY

  customer_id = resolve_vault_customer_id(tags, line)

  begin
    cek_wrapped = Base64.strict_decode64(tags[CEKA_TAG])
  rescue ArgumentError
    raise Error.new(
      code: ErrorCode::ERR_CENC_TAGS_MISSING,
      message: 'Invalid cenc_cek_wrapped_b64 Base64',
    )
  end

  unless cek_wrapped.bytesize == Config::RSA_2048_CIPHERTEXT_BYTES
    raise Error.new(
      code: ErrorCode::ERR_CENC_CEKA_WRAP,
      message: "Wrapped CEK must be #{Config::RSA_2048_CIPHERTEXT_BYTES} bytes",
    )
  end

  kid = tags[KID_TAG]
  kid = kid.strip.downcase if kid
  kid = nil if kid && kid.empty?

  CencMp4Metadata.new(
    customer_id: customer_id,
    cek_wrapped: cek_wrapped,
    kid_hex: kid,
    product_line: line,
    tag_deviceid: tag_value(tags, CUSTOMER_ID_FALLBACK_TAG) || tag_value(tags, CUSTOMER_ID_TAG),
    tag_host: tag_value(tags, HOST_TAG),
  )
end

.read_format_tags(mp4_path, ffprobe_executable: nil) ⇒ Object



47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
# File 'lib/robocap/sdk/mp4_cenc.rb', line 47

def read_format_tags(mp4_path, ffprobe_executable: nil)
  path = Pathname(mp4_path).expand_path
  unless path.file?
    raise Error.new(
      code: ErrorCode::ERR_CENC_TAGS_MISSING,
      message: "MP4 file not found: #{path}",
    )
  end
  exe = FfmpegCli.resolve_ffprobe_executable(ffprobe_executable)
  stdout, stderr, status = FfmpegCli.open3_capture3(
    exe, '-v', 'error', '-show_format', '-print_format', 'json', path.to_s,
  )
  unless status.exitstatus.zero?
    raise Error.new(
      code: ErrorCode::ERR_CENC_FFPROBE_FAILED,
      message: "ffprobe failed: #{stderr}",
    )
  end

  payload = parse_ffprobe_json(stdout)
  fmt = payload['format']
  unless fmt.is_a?(Hash)
    raise Error.new(
      code: ErrorCode::ERR_CENC_TAGS_MISSING,
      message: 'ffprobe output missing format section',
    )
  end
  tags = fmt['tags']
  unless tags.is_a?(Hash)
    raise Error.new(
      code: ErrorCode::ERR_CENC_TAGS_MISSING,
      message: 'MP4 has no format metadata tags',
    )
  end
  tags.transform_keys(&:to_s).transform_values(&:to_s)
end

.verify_session_device_id(session_device_id, tags, product_line) ⇒ Object

Binds a caller-supplied device id to the tags embedded in the MP4 so a capture cannot be decrypted under a different device's session.



154
155
156
157
158
159
160
161
162
163
164
165
166
# File 'lib/robocap/sdk/mp4_cenc.rb', line 154

def verify_session_device_id(session_device_id, tags, product_line)
  normalized = normalize_session_device_id(session_device_id)

  if product_line == PRODUCT_LINE_ROBOWRIST
    expected = tag_value(tags, HOST_TAG)
    field = HOST_TAG
  else
    expected = tag_value(tags, CUSTOMER_ID_TAG) || tag_value(tags, CUSTOMER_ID_FALLBACK_TAG)
    field = CUSTOMER_ID_TAG
  end

  assert_session_device_id_matches(expected, normalized, field)
end

.verify_session_device_id_from_metadata(session_device_id, meta) ⇒ Object



168
169
170
171
172
173
174
175
176
177
178
179
180
# File 'lib/robocap/sdk/mp4_cenc.rb', line 168

def (session_device_id, meta)
  normalized = normalize_session_device_id(session_device_id)

  if meta.product_line == PRODUCT_LINE_ROBOWRIST
    expected = meta.tag_host
    field = HOST_TAG
  else
    expected = meta.tag_deviceid
    field = CUSTOMER_ID_FALLBACK_TAG
  end

  assert_session_device_id_matches(expected, normalized, field)
end