Skip to content

Commit 48a829c

Browse files
authored
Merge pull request #351 from Hexer10/new-decipherer
[WIP] New signature decipherer
2 parents a2bc8d2 + 2e13c21 commit 48a829c

File tree

3 files changed

+111
-14
lines changed

3 files changed

+111
-14
lines changed

lib/src/js/js_engine.dart

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,15 +45,16 @@ class JSEngine {
4545
return context['return'];
4646
}
4747

48-
void resolveNode(Node node) {
48+
dynamic resolveNode(Node node) {
4949
return switch (node) {
5050
Statement() => resolveStatement(node),
5151
Expression() => resolveExpression(node),
52+
Name() => node.value,
5253
Node() => throw UnimplementedError('Unknown node type: $node'),
5354
};
5455
}
5556

56-
void resolveStatement(Statement statement) {
57+
dynamic resolveStatement(Statement statement) {
5758
return switch (statement) {
5859
VariableDeclaration() => resolveVariableDeclaration(statement),
5960
ExpressionStatement() => resolveExpression(statement.expression),
@@ -521,10 +522,21 @@ class JSEngine {
521522
IndexExpression() => resolveIndexExpression(expr),
522523
ConditionalExpression() => resolveConditionalExpression(expr),
523524
ThisExpression() => resolveThisExpression(),
525+
ObjectExpression() => resolveObjectExpression(expr),
524526
Expression() =>
525527
throw UnimplementedError('Unknown expression type: $expr'),
526528
};
527529
}
530+
531+
dynamic resolveObjectExpression(ObjectExpression expr) {
532+
final obj = <String, dynamic>{};
533+
for (final prop in expr.properties) {
534+
final key = resolveNode(prop.key);
535+
final value = resolveNode(prop.value);
536+
obj[key] = value;
537+
}
538+
return obj;
539+
}
528540
}
529541

530542
List<T> _splice<T>(List<T> array, int start,
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import 'package:freezed_annotation/freezed_annotation.dart';
2+
3+
import '../../js/js_engine.dart';
4+
5+
final _decFuncExp = RegExp(
6+
r'\b([a-zA-Z0-9_$]+)&&\(\1=([a-zA-Z0-9_$]{2,})\(decodeURIComponent\(\1\)\)',
7+
dotAll: true);
8+
9+
RegExp _funcExp(String funcName) => RegExp(
10+
'$funcName=(function.+?return.+?;)',
11+
dotAll: true,
12+
);
13+
14+
final _varsExp = RegExp(r'(?<!\()\b([a-zA-Z][a-zA-Z0-9_$]*)\b');
15+
16+
typedef DeciphererFunc = String Function(
17+
String sig,
18+
);
19+
20+
/// New implementation of the decipher due to the new YouTube cipher.
21+
@internal
22+
DeciphererFunc? getDecipherSignatureFunc(String? globalVar, String jscode) {
23+
final globalVarName =
24+
globalVar?.split('=')[0].trim().replaceFirst('var ', '');
25+
26+
final match = _decFuncExp.firstMatch(jscode);
27+
final funcName = match?.group(2);
28+
if (funcName == null) {
29+
return null;
30+
}
31+
32+
final decFunc = _funcExp(funcName)
33+
.firstMatch(jscode)
34+
?.group(1)
35+
?.replaceFirst('function', 'function $funcName');
36+
37+
if (decFunc == null) {
38+
return null;
39+
}
40+
41+
// Find all the vars used
42+
final vars = _varsExp.allMatches(decFunc).map((e) => e.group(1)).toSet();
43+
final keywords = ['function', 'return'];
44+
final varsList = vars
45+
.where(
46+
(e) => !keywords.contains(e) && e != funcName && e != globalVarName)
47+
.skip(1)
48+
.toList(); // Skip the first one, which is the function parameter
49+
50+
final varDecls = [];
51+
for (final varName in varsList) {
52+
final exp =
53+
RegExp(r'(var ' + varName! + r'=\{(?:.|\n)+?)(;var)', dotAll: true);
54+
final varMatch = exp.firstMatch(jscode);
55+
if (varMatch == null) {
56+
continue;
57+
}
58+
varDecls.add(varMatch.group(1)!);
59+
}
60+
61+
// Construct the final code
62+
var finalFunc = decFunc
63+
.replaceFirst('{', '{\n${varDecls.join('\n;')}\n')
64+
.replaceFirst('{', '{$globalVar');
65+
66+
return (String sig) => JSEngine.run(finalFunc, [sig]) as String;
67+
}

lib/src/videos/streams/stream_client.dart

Lines changed: 30 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import '../../exceptions/exceptions.dart';
66
import '../../extensions/helpers_extension.dart';
77
import '../../js/js_engine.dart';
88
import '../../retry.dart';
9+
import '../../reverse_engineering/cipher/chiper_new.dart';
910
import '../../reverse_engineering/cipher/cipher_manifest.dart';
1011
import '../../reverse_engineering/heuristics.dart';
1112
import '../../reverse_engineering/models/stream_info_provider.dart';
@@ -212,6 +213,19 @@ class StreamClient {
212213
}
213214

214215
String? _playerScript;
216+
String? _globalVar;
217+
218+
String? _getGlobalVar(String playerScript) {
219+
// Adapted from https://github.com/yt-dlp/yt-dlp/blob/7794374de8afb20499b023107e2abfd4e6b93ee4/yt_dlp/extractor/youtube/_video.py#L2295
220+
return _globalVar ??= _matchPatterns(playerScript, [
221+
(
222+
r'''
223+
(["\'])use\s+strict\1;\s*(var\s+[a-zA-Z0-9_$]+\s*=\s*((["\'])(?:(?!(\4)).|\\.)+\4\.split\((["\'])(?:(?!(\6)).)+\6\)|\[\s*(?:(["\'])(?:(?!(\8)).|\\.)*\8\s*,?\s*)+\]))[;,]
224+
''',
225+
2
226+
),
227+
]);
228+
}
215229

216230
Future<String> _getPlayerScript([WatchPage? page]) async {
217231
page ??= await WatchPage.get(_httpClient, '');
@@ -248,15 +262,7 @@ class StreamClient {
248262
'Could not find the decipher function in the player script.');
249263
}
250264

251-
// Adapted from https://github.com/yt-dlp/yt-dlp/blob/7794374de8afb20499b023107e2abfd4e6b93ee4/yt_dlp/extractor/youtube/_video.py#L2295
252-
final globalVar = _matchPatterns(playerScript, [
253-
(
254-
r'''
255-
(["\'])use\s+strict\1;\s*(var\s+[a-zA-Z0-9_$]+\s*=\s*((["\'])(?:(?!(\4)).|\\.)+\4\.split\((["\'])(?:(?!(\6)).)+\6\)|\[\s*(?:(["\'])(?:(?!(\8)).|\\.)*\8\s*,?\s*)+\]))[;,]
256-
''',
257-
2
258-
),
259-
]);
265+
final globalVar = _getGlobalVar(playerScript);
260266

261267
final func = funcMatch.replaceFirst('function', 'function main');
262268

@@ -299,16 +305,28 @@ class StreamClient {
299305
url = url.setQueryParam('n', deciphered);
300306
}
301307
if (stream.signatureParameter != null) {
302-
cipherManifest ??=
303-
CipherManifest.decode(await _getPlayerScript(watchPage));
308+
final playerScript = await _getPlayerScript(watchPage);
309+
cipherManifest ??= CipherManifest.decode(playerScript);
304310
final sig = stream.signature!;
305311
final sigParam = stream.signatureParameter!;
306312
if (cipherManifest != null) {
307313
final sigDeciphered = cipherManifest.decipher(sig);
308314
url = url.setQueryParam(sigParam, sigDeciphered);
309315
_logger.fine('Deciphered signature: $sig -> $sigDeciphered');
310316
} else {
311-
_logger.warning('Could not decipher signature: $sig');
317+
final globalVar = _getGlobalVar(playerScript);
318+
319+
final deciphererFunc =
320+
getDecipherSignatureFunc(globalVar, playerScript);
321+
final deciphered = deciphererFunc?.call(sig);
322+
if (deciphered != null) {
323+
url = url.setQueryParam(sigParam, deciphered);
324+
_logger.fine('[2] Deciphered signature: $sig -> $deciphered');
325+
} else {
326+
// If we cannot decipher the signature, we log a warning
327+
// and continue with the original URL.
328+
_logger.warning('Could not decipher signature: $sig');
329+
}
312330
}
313331
}
314332

0 commit comments

Comments
 (0)