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 +4 -4
- data/README.md +24 -0
- data/lib/robocap/sdk/decrypt_cenc.rb +9 -3
- data/lib/robocap/sdk/errors.rb +1 -0
- data/lib/robocap/sdk/mp4_cenc.rb +143 -20
- data/lib/robocap/sdk/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 79e885213cb75dc85172a8a8c8eb17baa9b3684ee6dff10e331d3e6262c2aefe
|
|
4
|
+
data.tar.gz: 84077253d5bf58dcf7c658512efc7274bf13c3cf6e30eaa87549ef0f73fa99e0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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,
|
|
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 =
|
|
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)
|
data/lib/robocap/sdk/errors.rb
CHANGED
data/lib/robocap/sdk/mp4_cenc.rb
CHANGED
|
@@ -9,12 +9,30 @@ require_relative 'ffmpeg_cli'
|
|
|
9
9
|
|
|
10
10
|
module RobocapCenc
|
|
11
11
|
module SDK
|
|
12
|
-
|
|
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 = '
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
|
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
|
-
|
|
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!(
|
|
257
|
+
Config.validate_customer_id!(normalized)
|
|
136
258
|
rescue ArgumentError
|
|
137
259
|
raise Error.new(
|
|
138
|
-
code: ErrorCode::
|
|
139
|
-
message: "Invalid
|
|
260
|
+
code: ErrorCode::ERR_DEVICE_BINDING_MISMATCH,
|
|
261
|
+
message: "Invalid session device id: #{normalized.inspect}",
|
|
140
262
|
)
|
|
141
263
|
end
|
|
142
|
-
|
|
264
|
+
normalized
|
|
143
265
|
end
|
|
144
266
|
|
|
145
|
-
def
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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
|
data/lib/robocap/sdk/version.rb
CHANGED