robocap-decryption-sdk 1.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 +38 -0
- data/exe/robocap-decryption-sdk +1 -1
- data/lib/robocap/sdk/cli.rb +4 -4
- data/lib/robocap/sdk/config.rb +1 -1
- data/lib/robocap/sdk/decrypt_cenc.rb +10 -4
- data/lib/robocap/sdk/errors.rb +2 -1
- data/lib/robocap/sdk/ffmpeg_cli.rb +1 -1
- data/lib/robocap/sdk/key_vault.rb +1 -1
- data/lib/robocap/sdk/mp4_cenc.rb +144 -21
- data/lib/robocap/sdk/ownership.rb +1 -1
- data/lib/robocap/sdk/rsa_delete.rb +1 -1
- data/lib/robocap/sdk/rsa_import.rb +1 -1
- data/lib/robocap/sdk/rsa_key_meta.rb +1 -1
- data/lib/robocap/sdk/rsa_oaep.rb +1 -1
- data/lib/robocap/sdk/vault_layout.rb +1 -1
- data/lib/robocap/sdk/version.rb +2 -2
- data/lib/robocap/sdk.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
|
@@ -32,6 +32,44 @@ bundle exec exe/robocap-decryption-sdk delete-rsa --help
|
|
|
32
32
|
bundle exec exe/robocap-decryption-sdk decrypt-cenc --help
|
|
33
33
|
```
|
|
34
34
|
|
|
35
|
+
## Ruby API
|
|
36
|
+
|
|
37
|
+
```ruby
|
|
38
|
+
require 'robocap/sdk'
|
|
39
|
+
|
|
40
|
+
RobocapCenc::SDK.decrypt_cenc_mp4(...)
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Changelog
|
|
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
|
+
|
|
69
|
+
### 2.0.0
|
|
70
|
+
|
|
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::*`.
|
|
72
|
+
|
|
35
73
|
## Verified against the Python SDK
|
|
36
74
|
|
|
37
75
|
This Ruby gem reads and writes the same on-disk vault and CENC MP4 format
|
data/exe/robocap-decryption-sdk
CHANGED
data/lib/robocap/sdk/cli.rb
CHANGED
|
@@ -6,7 +6,7 @@ require 'pathname'
|
|
|
6
6
|
require 'time'
|
|
7
7
|
require_relative '../sdk'
|
|
8
8
|
|
|
9
|
-
module
|
|
9
|
+
module RobocapCenc
|
|
10
10
|
module SDK
|
|
11
11
|
module CLI
|
|
12
12
|
module_function
|
|
@@ -75,7 +75,7 @@ module Robocap
|
|
|
75
75
|
device_id: opts[:device_id] || opts[:customer_id],
|
|
76
76
|
rsa_bits: opts[:rsa_bits],
|
|
77
77
|
)
|
|
78
|
-
result =
|
|
78
|
+
result = RobocapCenc::SDK.import_rsa_key_version(
|
|
79
79
|
customer_id: opts[:customer_id],
|
|
80
80
|
public_pem: opts[:public_key].binread,
|
|
81
81
|
private_pem: opts[:private_key].binread,
|
|
@@ -100,7 +100,7 @@ module Robocap
|
|
|
100
100
|
end.parse!(argv)
|
|
101
101
|
|
|
102
102
|
require_opts!(opts, %i[customer_id rsa_key_version])
|
|
103
|
-
result =
|
|
103
|
+
result = RobocapCenc::SDK.delete_rsa_key_version(
|
|
104
104
|
customer_id: opts[:customer_id],
|
|
105
105
|
rsa_key_version: opts[:rsa_key_version],
|
|
106
106
|
sdk_root: opts[:sdk_root],
|
|
@@ -127,7 +127,7 @@ module Robocap
|
|
|
127
127
|
|
|
128
128
|
require_opts!(opts, %i[mp4_path private_key output_dir])
|
|
129
129
|
opts[:output_dir].mkpath
|
|
130
|
-
result =
|
|
130
|
+
result = RobocapCenc::SDK.decrypt_cenc_mp4(
|
|
131
131
|
mp4_path: opts[:mp4_path],
|
|
132
132
|
user_private_pem: opts[:private_key].binread,
|
|
133
133
|
output_dir: opts[:output_dir],
|
data/lib/robocap/sdk/config.rb
CHANGED
|
@@ -9,19 +9,25 @@ require_relative 'ownership'
|
|
|
9
9
|
require_relative 'ffmpeg_cli'
|
|
10
10
|
require_relative 'vault_layout'
|
|
11
11
|
|
|
12
|
-
module
|
|
12
|
+
module RobocapCenc
|
|
13
13
|
module SDK
|
|
14
14
|
DecryptCencResult = Data.define(:output_path, :customer_id, :rsa_key_version, :kid_hex)
|
|
15
15
|
|
|
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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
module
|
|
3
|
+
module RobocapCenc
|
|
4
4
|
module SDK
|
|
5
5
|
module ErrorCode
|
|
6
6
|
ERR_CUSTOMER_NOT_FOUND = 1001
|
|
@@ -33,6 +33,7 @@ module Robocap
|
|
|
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)
|
data/lib/robocap/sdk/mp4_cenc.rb
CHANGED
|
@@ -7,14 +7,32 @@ require_relative 'config'
|
|
|
7
7
|
require_relative 'errors'
|
|
8
8
|
require_relative 'ffmpeg_cli'
|
|
9
9
|
|
|
10
|
-
module
|
|
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 Robocap
|
|
|
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 Robocap
|
|
|
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 Robocap
|
|
|
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 Robocap
|
|
|
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 Robocap
|
|
|
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
|
|
@@ -4,7 +4,7 @@ require 'json'
|
|
|
4
4
|
require 'time'
|
|
5
5
|
require_relative 'config'
|
|
6
6
|
|
|
7
|
-
module
|
|
7
|
+
module RobocapCenc
|
|
8
8
|
module SDK
|
|
9
9
|
class RsaKeyMeta < Data.define(:rsa_key_version, :effective_at, :device_id, :rsa_bits)
|
|
10
10
|
def initialize(rsa_key_version:, effective_at:, device_id:, rsa_bits: Config::RSA_BITS)
|
data/lib/robocap/sdk/rsa_oaep.rb
CHANGED
data/lib/robocap/sdk/version.rb
CHANGED
data/lib/robocap/sdk.rb
CHANGED