11'use strict' ;
22
33const {
4+ ArrayIsArray,
45 ArrayPrototypeSlice,
56 FunctionPrototypeCall,
67 JSONParse,
78 JSONStringify,
89 ObjectDefineProperties,
9- PromisePrototypeThen,
10+ ObjectDefineProperty,
11+ ObjectKeys,
12+ ObjectSetPrototypeOf,
1013 PromiseReject,
1114 PromiseResolve,
1215 ReflectApply,
1316 ReflectConstruct,
17+ SafeArrayIterator,
1418 StringPrototypeRepeat,
1519 StringPrototypeSlice,
1620 StringPrototypeStartsWith,
21+ SymbolIterator,
1722 SymbolToStringTag,
1823 TypedArrayPrototypeGetBuffer,
1924} = primordials ;
@@ -26,7 +31,10 @@ const {
2631 kWebCryptoCipherDecrypt,
2732} = internalBinding ( 'crypto' ) ;
2833
29- const { TextDecoder, TextEncoder } = require ( 'internal/encoding' ) ;
34+ const {
35+ decodeUTF8,
36+ encodeUtf8String,
37+ } = internalBinding ( 'encoding_binding' ) ;
3038
3139const {
3240 codes : {
@@ -55,6 +63,7 @@ const {
5563
5664const {
5765 getBlockSize,
66+ jobPromiseThen,
5867 normalizeAlgorithm,
5968 normalizeHashName,
6069 validateMaxBufferLength,
@@ -71,13 +80,20 @@ const {
7180 randomUUID : _randomUUID ,
7281} = require ( 'internal/crypto/random' ) ;
7382
83+ const {
84+ isPromise,
85+ } = require ( 'internal/util/types' ) ;
86+
7487let webidl ;
7588
7689// WebCrypto methods return promises, including for synchronous validation
7790// failures. Keep that conversion in one place so method bodies stay readable.
7891function callSubtleCryptoMethod ( fn , receiver , args ) {
7992 try {
80- return PromiseResolve ( ReflectApply ( fn , receiver , args ) ) ;
93+ const result = ReflectApply ( fn , receiver , args ) ;
94+ // PromiseResolve(promise) reads promise.constructor. Avoid re-wrapping
95+ // native promises so Promise.prototype.constructor pollution is ignored.
96+ return isPromise ( result ) ? result : PromiseResolve ( result ) ;
8197 } catch ( err ) {
8298 return PromiseReject ( err ) ;
8399 }
@@ -384,7 +400,7 @@ function deriveKeyImpl(
384400 throw lazyDOMException ( 'Unrecognized algorithm name' , 'NotSupportedError' ) ;
385401 }
386402
387- return PromisePrototypeThen ( bits , ( bits ) => FunctionPrototypeCall (
403+ return jobPromiseThen ( bits , ( bits ) => FunctionPrototypeCall (
388404 importKeySync ,
389405 this ,
390406 'raw-secret' , bits , derivedKeyAlgorithm , extractable , keyUsages ,
@@ -748,6 +764,64 @@ function exportKeyImpl(format, key) {
748764 return exportKeySync ( format , key ) ;
749765}
750766
767+ // Parsed JWK arrays are detached from Array.prototype but still need to pass
768+ // WebIDL sequence conversion, which reads @@iterator from the value.
769+ function safeArrayIterator ( ) {
770+ return new SafeArrayIterator ( this ) ;
771+ }
772+
773+ // The WebCrypto spec parses and stringifies JWKs in a fresh global object.
774+ // Detach internal JSON values from the current global's mutable prototypes to
775+ // approximate those fresh-realm semantics without creating a new realm.
776+ function detachFromUserPrototypes ( value ) {
777+ if ( value === null || typeof value !== 'object' )
778+ return ;
779+
780+ ObjectSetPrototypeOf ( value , null ) ;
781+
782+ if ( ArrayIsArray ( value ) ) {
783+ ObjectDefineProperty ( value , SymbolIterator , {
784+ __proto__ : null ,
785+ configurable : true ,
786+ value : safeArrayIterator ,
787+ } ) ;
788+ for ( let n = 0 ; n < value . length ; n ++ )
789+ detachFromUserPrototypes ( value [ n ] ) ;
790+ return ;
791+ }
792+
793+ const keys = ObjectKeys ( value ) ;
794+ for ( let n = 0 ; n < keys . length ; n ++ )
795+ detachFromUserPrototypes ( value [ keys [ n ] ] ) ;
796+ }
797+
798+ // Parse wrapped JWK bytes according to WebCrypto's "parse a JWK" procedure.
799+ function parseJwk ( keyData ) {
800+ let key ;
801+ try {
802+ // WebCrypto parses JWKs in a fresh global. Detach parsed JSON values
803+ // from user-mutated prototypes before WebIDL dictionary conversion.
804+ // Wrapped JWKs may be produced outside WebCrypto, so parse using the
805+ // spec-required UTF-8.
806+ const json = decodeUTF8 ( keyData , false , true ) ;
807+ const result = JSONParse ( json ) ;
808+ detachFromUserPrototypes ( result ) ;
809+ key = webidl . converters . JsonWebKey ( result ) ;
810+ } catch ( err ) {
811+ throw lazyDOMException (
812+ 'Invalid wrapped JWK key' ,
813+ { name : 'DataError' , cause : err } ) ;
814+ }
815+
816+ if ( key . kty === undefined ) {
817+ throw lazyDOMException (
818+ 'Invalid wrapped JWK key' ,
819+ 'DataError' ) ;
820+ }
821+
822+ return key ;
823+ }
824+
751825function aliasKeyFormat ( format ) {
752826 switch ( format ) {
753827 case 'raw-public' :
@@ -967,15 +1041,21 @@ function wrapKeyImpl(format, key, wrappingKey, algorithm) {
9671041 let keyData = exportKeySync ( format , key ) ;
9681042
9691043 if ( format === 'jwk' ) {
970- const ec = new TextEncoder ( ) ;
971- const raw = JSONStringify ( keyData ) ;
1044+ // The WebCrypto spec stringifies JWKs in a new global object. Rather
1045+ // than create a new realm here, detach this internally generated JWK from
1046+ // user-mutated prototypes so JSON.stringify cannot read inherited toJSON
1047+ // hooks from the current global.
1048+ detachFromUserPrototypes ( keyData ) ;
1049+ const json = JSONStringify ( keyData ) ;
9721050 // As per the NOTE in step 13 https://w3c.github.io/webcrypto/#SubtleCrypto-method-wrapKey
9731051 // we're padding AES-KW wrapped JWK to make sure it is always a multiple of 8 bytes
9741052 // in length
975- if ( algorithm . name === 'AES-KW' && raw . length % 8 !== 0 ) {
976- keyData = ec . encode ( raw + StringPrototypeRepeat ( ' ' , 8 - ( raw . length % 8 ) ) ) ;
1053+ // The spec then UTF-8 encodes json.
1054+ if ( algorithm . name === 'AES-KW' && json . length % 8 !== 0 ) {
1055+ keyData = encodeUtf8String (
1056+ json + StringPrototypeRepeat ( ' ' , 8 - ( json . length % 8 ) ) ) ;
9771057 } else {
978- keyData = ec . encode ( raw ) ;
1058+ keyData = encodeUtf8String ( json ) ;
9791059 }
9801060 }
9811061
@@ -1067,17 +1147,9 @@ function unwrapKeyImpl(
10671147 wrappedKey ,
10681148 'unwrapKey' ) ;
10691149
1070- return PromisePrototypeThen ( keyData , ( keyData ) => {
1150+ return jobPromiseThen ( keyData , ( keyData ) => {
10711151 if ( format === 'jwk' ) {
1072- // The fatal: true option is only supported in builds that have ICU.
1073- const options = process . versions . icu !== undefined ?
1074- { fatal : true } : undefined ;
1075- const dec = new TextDecoder ( 'utf-8' , options ) ;
1076- try {
1077- keyData = JSONParse ( dec . decode ( keyData ) ) ;
1078- } catch {
1079- throw lazyDOMException ( 'Invalid wrapped JWK key' , 'DataError' ) ;
1080- }
1152+ keyData = parseJwk ( keyData ) ;
10811153 }
10821154
10831155 return FunctionPrototypeCall (
@@ -1453,7 +1525,7 @@ function encapsulateKeyImpl(
14531525 throw lazyDOMException ( 'Unrecognized algorithm name' , 'NotSupportedError' ) ;
14541526 }
14551527
1456- return PromisePrototypeThen ( encapsulateBits , ( encapsulateBits ) => {
1528+ return jobPromiseThen ( encapsulateBits , ( encapsulateBits ) => {
14571529 const sharedKey = FunctionPrototypeCall (
14581530 importKeySync ,
14591531 this ,
@@ -1593,7 +1665,7 @@ function decapsulateKeyImpl(
15931665 throw lazyDOMException ( 'Unrecognized algorithm name' , 'NotSupportedError' ) ;
15941666 }
15951667
1596- return PromisePrototypeThen ( decapsulatedBits , ( decapsulatedBits ) => FunctionPrototypeCall (
1668+ return jobPromiseThen ( decapsulatedBits , ( decapsulatedBits ) => FunctionPrototypeCall (
15971669 importKeySync ,
15981670 this ,
15991671 'raw-secret' , decapsulatedBits , normalizedSharedKeyAlgorithm , extractable ,
0 commit comments