robocap-decryption-sdk 2.0.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 024c14dc83b71450c45bd140f4df42fcfacda47f71d4f04edf56d8989e768dd2
4
- data.tar.gz: 55c76d3298de9561d29a8a877e017f7157dec1b17b5d6c22486b17719ab11a13
3
+ metadata.gz: 79e885213cb75dc85172a8a8c8eb17baa9b3684ee6dff10e331d3e6262c2aefe
4
+ data.tar.gz: 84077253d5bf58dcf7c658512efc7274bf13c3cf6e30eaa87549ef0f73fa99e0
5
5
  SHA512:
6
- metadata.gz: f26a53d8ec725d1c8ded062db1ba81a5dcb86de1848de0adb3261213c6616067c997cca2605b804057101a07d37fa0e94fa6208d87ecf5bf3b944ac51a12c427
7
- data.tar.gz: e0bd269cb297024bb493437a26c9b5eb52b8058b94a0dce5edcd497e0453c959be134608102710b5e94af3fade04e48ff1d979317f2be159f2a69daa0ca90f0d
6
+ metadata.gz: 1ada52a6ef96f5a2a0726e97365b48c6f0452e84f09d5916269aacc94fc8e3fedfdc8da924396baff5fb45aafe9ed5aad6a364684b31357425491895bd02d7eb
7
+ data.tar.gz: 449d464f2324500cd1409ed6b3d42dcaa639b2d482614db00432671e2a8afc33393711534db9ad9c7cc8f56222dee130a14fae582e8da96ad1836d150367b371
data/README.md CHANGED
@@ -42,6 +42,30 @@ RobocapCenc::SDK.decrypt_cenc_mp4(...)
42
42
 
43
43
  ## Changelog
44
44
 
45
+ ### 2.1.0
46
+
47
+ Brings the Ruby SDK to parity with the Python SDK's device/host vault
48
+ resolution. See [`../spec/cenc-tags.md`](../spec/cenc-tags.md) for the full
49
+ contract.
50
+
51
+ BREAKING: the `username` MP4 tag is no longer a customer-id source. The
52
+ fallback tag is now `deviceid`. A capture whose only customer-id source is
53
+ `username` now fails `ERR_CENC_TAGS_MISSING` — re-tag it with `deviceid`.
54
+
55
+ - Product line is detected from the MP4 filename stem (`robowrist_` /
56
+ `robocap_` / otherwise legacy) via `Mp4Cenc.detect_product_line`.
57
+ - `robowrist` captures resolve their vault customer id from the `host` tag
58
+ instead of the device id. Import robowrist keys under the **host**.
59
+ - `CencMp4Metadata` gained `product_line`, `tag_deviceid`, and `tag_host`.
60
+ - New `ERR_DEVICE_BINDING_MISMATCH` (7012) plus
61
+ `Mp4Cenc.verify_session_device_id` / `verify_session_device_id_from_metadata`.
62
+ - `DecryptCenc.call` accepts `metadata:` (skip re-probing an already-parsed
63
+ file) and `session_device_id:` (bind decryption to one device).
64
+ - `parse_cenc_metadata_from_tags` now takes optional `mp4_path:` /
65
+ `product_line:` keywords. Callers passing a **literal** bare hash must brace
66
+ it — `parse_cenc_metadata_from_tags({ 'k' => 'v' })` — since Ruby would
67
+ otherwise read it as keyword arguments. Passing a hash variable is unaffected.
68
+
45
69
  ### 2.0.0
46
70
 
47
71
  BREAKING: Ruby namespace renamed `Robocap::SDK` -> `RobocapCenc::SDK` to avoid colliding with host apps that define a top-level `Robocap` constant. Gem name, require path, and CLI executable are unchanged. Ruby API consumers must update `Robocap::SDK::*` references to `RobocapCenc::SDK::*`.
@@ -16,12 +16,18 @@ module RobocapCenc
16
16
  module DecryptCenc
17
17
  module_function
18
18
 
19
- def call(mp4_path:, user_private_pem:, output_dir:,
20
- sdk_root: nil, ffprobe_executable: nil, ffmpeg_executable: nil)
19
+ def call(mp4_path:, user_private_pem:, output_dir:, metadata: nil,
20
+ sdk_root: nil, session_device_id: nil,
21
+ ffprobe_executable: nil, ffmpeg_executable: nil)
21
22
  mp4_path = Pathname(mp4_path)
22
23
  output_dir = Pathname(output_dir)
23
24
 
24
- meta = Mp4Cenc.load_cenc_metadata(mp4_path, ffprobe_executable: ffprobe_executable)
25
+ meta = metadata ||
26
+ Mp4Cenc.load_cenc_metadata(mp4_path, ffprobe_executable: ffprobe_executable)
27
+
28
+ unless session_device_id.nil?
29
+ Mp4Cenc.verify_session_device_id_from_metadata(session_device_id, meta)
30
+ end
25
31
 
26
32
  root = Pathname(sdk_root || Config.default_sdk_root)
27
33
  vault = KeyVault.new(root)
@@ -33,6 +33,7 @@ module RobocapCenc
33
33
  ERR_CENC_FFPROBE_FAILED = 7009
34
34
  ERR_CENC_CUSTOMER_ID_INVALID = 7010
35
35
  ERR_CENC_CEKA_TRIAL_FAILED = 7011
36
+ ERR_DEVICE_BINDING_MISMATCH = 7012
36
37
 
37
38
  NAMES = constants.each_with_object({}) do |c, h|
38
39
  v = const_get(c)
@@ -9,12 +9,30 @@ require_relative 'ffmpeg_cli'
9
9
 
10
10
  module RobocapCenc
11
11
  module SDK
12
- CencMp4Metadata = Data.define(:customer_id, :cek_wrapped, :kid_hex)
12
+ PRODUCT_LINE_ROBOCAP = 'robocap'
13
+ PRODUCT_LINE_ROBOWRIST = 'robowrist'
14
+ PRODUCT_LINE_LEGACY = 'legacy'
15
+
16
+ PRODUCT_LINES = [
17
+ PRODUCT_LINE_ROBOCAP,
18
+ PRODUCT_LINE_ROBOWRIST,
19
+ PRODUCT_LINE_LEGACY,
20
+ ].freeze
21
+
22
+ CencMp4Metadata = Data.define(
23
+ :customer_id, :cek_wrapped, :kid_hex, :product_line, :tag_deviceid, :tag_host,
24
+ ) do
25
+ def initialize(customer_id:, cek_wrapped:, kid_hex: nil,
26
+ product_line: PRODUCT_LINE_LEGACY, tag_deviceid: nil, tag_host: nil)
27
+ super
28
+ end
29
+ end
13
30
 
14
31
  module Mp4Cenc
15
32
  CEKA_TAG = 'cenc_cek_wrapped_b64'
16
33
  CUSTOMER_ID_TAG = 'cenc_customer_id'
17
- CUSTOMER_ID_FALLBACK_TAG = 'username'
34
+ CUSTOMER_ID_FALLBACK_TAG = 'deviceid'
35
+ HOST_TAG = 'host'
18
36
  KID_TAG = 'cenc_kid_hex'
19
37
 
20
38
  CENC_WRAPPED_ALGO_TAG = 'cenc_wrapped_algo'
@@ -63,7 +81,16 @@ module RobocapCenc
63
81
  tags.transform_keys(&:to_s).transform_values(&:to_s)
64
82
  end
65
83
 
66
- def parse_cenc_metadata_from_tags(tags)
84
+ # Product line is derived from the filename prefix, which decides whether
85
+ # the vault is keyed by the +deviceid+ or the +host+ tag.
86
+ def detect_product_line(mp4_path)
87
+ stem = Pathname(mp4_path).basename('.*').to_s.downcase
88
+ return PRODUCT_LINE_ROBOWRIST if stem.start_with?('robowrist_')
89
+ return PRODUCT_LINE_ROBOCAP if stem.start_with?('robocap_')
90
+ PRODUCT_LINE_LEGACY
91
+ end
92
+
93
+ def parse_cenc_metadata_from_tags(tags, mp4_path: nil, product_line: nil)
67
94
  unless tags[CEKA_TAG] && !tags[CEKA_TAG].empty?
68
95
  raise Error.new(
69
96
  code: ErrorCode::ERR_CENC_TAGS_MISSING,
@@ -71,7 +98,10 @@ module RobocapCenc
71
98
  )
72
99
  end
73
100
 
74
- customer_id = resolve_customer_id(tags)
101
+ line = product_line
102
+ line ||= mp4_path ? detect_product_line(mp4_path) : PRODUCT_LINE_LEGACY
103
+
104
+ customer_id = resolve_vault_customer_id(tags, line)
75
105
 
76
106
  begin
77
107
  cek_wrapped = Base64.strict_decode64(tags[CEKA_TAG])
@@ -93,23 +123,62 @@ module RobocapCenc
93
123
  kid = kid.strip.downcase if kid
94
124
  kid = nil if kid && kid.empty?
95
125
 
96
- CencMp4Metadata.new(customer_id: customer_id, cek_wrapped: cek_wrapped, kid_hex: kid)
126
+ CencMp4Metadata.new(
127
+ customer_id: customer_id,
128
+ cek_wrapped: cek_wrapped,
129
+ kid_hex: kid,
130
+ product_line: line,
131
+ tag_deviceid: tag_value(tags, CUSTOMER_ID_FALLBACK_TAG) || tag_value(tags, CUSTOMER_ID_TAG),
132
+ tag_host: tag_value(tags, HOST_TAG),
133
+ )
97
134
  end
98
135
 
99
136
  def load_cenc_metadata(mp4_path, ffprobe_executable: nil)
137
+ path = Pathname(mp4_path).expand_path
138
+ tags = read_format_tags(path, ffprobe_executable: ffprobe_executable)
100
139
  parse_cenc_metadata_from_tags(
101
- read_format_tags(mp4_path, ffprobe_executable: ffprobe_executable),
140
+ tags, mp4_path: path, product_line: detect_product_line(path),
102
141
  )
103
142
  end
104
143
 
105
144
  def has_cenc_tags(mp4_path, ffprobe_executable: nil)
106
- tags = read_format_tags(mp4_path, ffprobe_executable: ffprobe_executable)
107
- return false unless tags[CEKA_TAG] && !tags[CEKA_TAG].empty?
108
- has_customer_id_source?(tags)
145
+ path = Pathname(mp4_path).expand_path
146
+ tags = read_format_tags(path, ffprobe_executable: ffprobe_executable)
147
+ has_required_tags_for_product?(tags, detect_product_line(path))
109
148
  rescue Error
110
149
  false
111
150
  end
112
151
 
152
+ # Binds a caller-supplied device id to the tags embedded in the MP4 so a
153
+ # capture cannot be decrypted under a different device's session.
154
+ def verify_session_device_id(session_device_id, tags, product_line)
155
+ normalized = normalize_session_device_id(session_device_id)
156
+
157
+ if product_line == PRODUCT_LINE_ROBOWRIST
158
+ expected = tag_value(tags, HOST_TAG)
159
+ field = HOST_TAG
160
+ else
161
+ expected = tag_value(tags, CUSTOMER_ID_TAG) || tag_value(tags, CUSTOMER_ID_FALLBACK_TAG)
162
+ field = CUSTOMER_ID_TAG
163
+ end
164
+
165
+ assert_session_device_id_matches(expected, normalized, field)
166
+ end
167
+
168
+ def verify_session_device_id_from_metadata(session_device_id, meta)
169
+ normalized = normalize_session_device_id(session_device_id)
170
+
171
+ if meta.product_line == PRODUCT_LINE_ROBOWRIST
172
+ expected = meta.tag_host
173
+ field = HOST_TAG
174
+ else
175
+ expected = meta.tag_deviceid
176
+ field = CUSTOMER_ID_FALLBACK_TAG
177
+ end
178
+
179
+ assert_session_device_id_matches(expected, normalized, field)
180
+ end
181
+
113
182
  class << self
114
183
  private
115
184
 
@@ -122,7 +191,24 @@ module RobocapCenc
122
191
  )
123
192
  end
124
193
 
125
- def resolve_customer_id(tags)
194
+ def tag_value(tags, key)
195
+ raw = tags[key]
196
+ return nil if raw.nil?
197
+ stripped = raw.strip
198
+ stripped.empty? ? nil : stripped
199
+ end
200
+
201
+ def validate_customer_id_string(customer_id)
202
+ Config.validate_customer_id!(customer_id)
203
+ customer_id
204
+ rescue ArgumentError
205
+ raise Error.new(
206
+ code: ErrorCode::ERR_CENC_CUSTOMER_ID_INVALID,
207
+ message: "Invalid customer id: #{customer_id.inspect}",
208
+ )
209
+ end
210
+
211
+ def resolve_deviceid_from_tags(tags)
126
212
  raw = tags[CUSTOMER_ID_TAG] || tags[CUSTOMER_ID_FALLBACK_TAG]
127
213
  if raw.nil? || raw.strip.empty?
128
214
  raise Error.new(
@@ -130,23 +216,60 @@ module RobocapCenc
130
216
  message: "Missing CENC customer id: #{CUSTOMER_ID_TAG} or #{CUSTOMER_ID_FALLBACK_TAG} tag required",
131
217
  )
132
218
  end
133
- customer = raw.strip
219
+ validate_customer_id_string(raw.strip)
220
+ end
221
+
222
+ def resolve_vault_customer_id(tags, product_line)
223
+ if product_line == PRODUCT_LINE_ROBOWRIST
224
+ host = tag_value(tags, HOST_TAG)
225
+ if host.nil?
226
+ raise Error.new(
227
+ code: ErrorCode::ERR_CENC_TAGS_MISSING,
228
+ message: "Missing CENC host tag for robowrist: #{HOST_TAG} required",
229
+ )
230
+ end
231
+ return validate_customer_id_string(host)
232
+ end
233
+ resolve_deviceid_from_tags(tags)
234
+ end
235
+
236
+ def has_customer_id_source?(tags)
237
+ [CUSTOMER_ID_TAG, CUSTOMER_ID_FALLBACK_TAG].any? { |k| !tag_value(tags, k).nil? }
238
+ end
239
+
240
+ def has_required_tags_for_product?(tags, product_line)
241
+ return false unless tags[CEKA_TAG] && !tags[CEKA_TAG].empty?
242
+ if product_line == PRODUCT_LINE_ROBOWRIST
243
+ return !tag_value(tags, HOST_TAG).nil? && has_customer_id_source?(tags)
244
+ end
245
+ has_customer_id_source?(tags)
246
+ end
247
+
248
+ def normalize_session_device_id(session_device_id)
249
+ normalized = session_device_id.to_s.strip
250
+ if normalized.empty?
251
+ raise Error.new(
252
+ code: ErrorCode::ERR_DEVICE_BINDING_MISMATCH,
253
+ message: 'Session device id is empty',
254
+ )
255
+ end
134
256
  begin
135
- Config.validate_customer_id!(customer)
257
+ Config.validate_customer_id!(normalized)
136
258
  rescue ArgumentError
137
259
  raise Error.new(
138
- code: ErrorCode::ERR_CENC_CUSTOMER_ID_INVALID,
139
- message: "Invalid customer id: #{customer.inspect}",
260
+ code: ErrorCode::ERR_DEVICE_BINDING_MISMATCH,
261
+ message: "Invalid session device id: #{normalized.inspect}",
140
262
  )
141
263
  end
142
- customer
264
+ normalized
143
265
  end
144
266
 
145
- def has_customer_id_source?(tags)
146
- [CUSTOMER_ID_TAG, CUSTOMER_ID_FALLBACK_TAG].any? do |k|
147
- v = tags[k]
148
- v && !v.strip.empty?
149
- end
267
+ def assert_session_device_id_matches(expected, normalized, field)
268
+ return if !expected.nil? && expected == normalized
269
+ raise Error.new(
270
+ code: ErrorCode::ERR_DEVICE_BINDING_MISMATCH,
271
+ message: "Session device id does not match MP4 #{field} tag",
272
+ )
150
273
  end
151
274
  end
152
275
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module RobocapCenc
4
4
  module SDK
5
- VERSION = '2.0.0'
5
+ VERSION = '2.1.0'
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: robocap-decryption-sdk
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.0
4
+ version: 2.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Frodobots