11import assert from 'node:assert' ;
22import https from 'node:https' ;
3- import crypto from 'node:crypto' ;
43import { URL , URLSearchParams } from 'node:url' ;
5- import KeygripAutorotate from 'keygrip-autorotate' ;
4+ import { KeygripAutorotate , generateRandomBytes } from 'keygrip-autorotate' ;
65
7- const STATE_RANDOM_BYTES = 5 ;
8- const STATE_RANDOM_BYTES_STRING_LENGTH = STATE_RANDOM_BYTES * 2 ;
9- const MIN_ACCEPTED_STATE_LENGTH = STATE_RANDOM_BYTES_STRING_LENGTH + 29 ;
10- const MAX_ACCEPTED_STATE_LENGTH = 3500 ;
6+ const STATE_SALT_MIN_BYTES = 64 ;
7+ const STATE_SALT_MAX_BYTES = 128 ;
8+ const STATE_MIN_PLAUSIBLE_LENGTH = STATE_SALT_MIN_BYTES + 6 ; // just a rough number, base64 overhead and email payload not even included
119const DEFAULT_MAX_LOGIN_PROCESS_DURATION = 2 * 60 * 1000 ; // max. 2 minutes to enter credentials & hit Authorize
1210const DEFAULT_SCOPES = [ 'user:email' ] ;
13- const ANY_EMAIL_PLACEHOLDER = '@' + crypto . randomBytes ( 10 ) . toString ( 'hex' ) ;
14-
15- /**
16- * @param {string } s
17- * @return {string }
18- */
19- export function encodeSafeBase64 ( s ) {
20- return Buffer . from ( s , 'utf8' ) . toString ( 'base64' ) . replace ( / [ / + = ] / g, s => s === '/' ? '_' : s === '+' ? '-' : '' ) ;
21- }
22-
23- /**
24- * @param {string } s
25- * @return {?string }
26- */
27- export function decodeUrlSafeBase64 ( s ) {
28- if ( typeof s !== 'string' ) {
29- return null ;
30- }
31- return Buffer . from ( s . replace ( / [ - _ ] / g, s => s === '-' ? '+' : s === '_' ? '/' : '' ) , 'base64' ) . toString ( 'utf8' ) ;
32- }
11+ const ANY_EMAIL_PLACEHOLDER = '@' + generateRandomBytes ( 10 , 20 ) . toString ( 'base64url' ) ;
3312
3413/**
3514 * @param {string } payload - the plaintext value to put into the state
3615 * @param {KeygripAutorotate } signer
3716 * @return {string } a url-safe, signed state string that can be verified+decoded using {@link getPayloadFromStateIfVerified}
3817 */
3918export function createSignedStateForPayload ( payload , signer ) {
40- let encodedPayload = encodeSafeBase64 ( payload || '' ) ,
41- randomPrefix = crypto . randomBytes ( STATE_RANDOM_BYTES ) . toString ( 'hex' ) ,
42- textToSign = encodedPayload . length . toString ( 36 ) + '_' + randomPrefix + encodedPayload ;
19+ let encodedPayload = Buffer . from ( payload || '' ) . toString ( 'base64url' ) ,
20+ encodedSalt = generateRandomBytes ( STATE_SALT_MIN_BYTES , STATE_SALT_MAX_BYTES ) . toString ( 'base64url' ) ,
21+ textToSign = encodedPayload . length . toString ( 36 ) . padStart ( 3 , '0' ) +
22+ encodedSalt . length . toString ( 36 ) . padStart ( 3 , '0' ) + encodedPayload + encodedSalt ;
4323
4424 return textToSign + signer . sign ( textToSign ) ;
4525}
@@ -51,26 +31,21 @@ export function createSignedStateForPayload(payload, signer) {
5131 * @return {?string } the payload from the signed state IF the state could be verified, otherwise null
5232 */
5333export function getPayloadFromStateIfVerified ( state , signer ) {
54- if ( ! state || typeof state !== 'string' || state . length < MIN_ACCEPTED_STATE_LENGTH || state . length > MAX_ACCEPTED_STATE_LENGTH ) {
34+ if ( typeof state !== 'string' || state . length < STATE_MIN_PLAUSIBLE_LENGTH || state . length > 3500 ) {
5535 return null ;
5636 }
5737 let payload = null ;
5838 try {
59- let lengthDividerIndex = state . indexOf ( '_' ) ;
60- if ( lengthDividerIndex > 3 ) {
61- // encoded payload length can never be 36^4+
62- return null ;
63- }
64- let encodedPayloadLength = parseInt ( state . substr ( 0 , lengthDividerIndex ) , 36 ) ,
65- encodedPayload = state . substr ( lengthDividerIndex + 1 + STATE_RANDOM_BYTES_STRING_LENGTH , encodedPayloadLength ) ,
66- signedTextLength = lengthDividerIndex + 1 + STATE_RANDOM_BYTES_STRING_LENGTH + encodedPayloadLength ,
67- signedText = state . substr ( 0 , signedTextLength ) ,
68- signature = state . substr ( signedTextLength ) ;
39+ let encodedPayloadLength = parseInt ( state . substring ( 0 , 3 ) , 36 ) ,
40+ encodedSaltLength = parseInt ( state . substring ( 3 , 6 ) , 36 ) ,
41+ signedTextLength = 6 + encodedPayloadLength + encodedSaltLength ,
42+ encodedPayload = state . substring ( 6 , 6 + encodedPayloadLength ) ,
43+ signedText = state . substring ( 0 , signedTextLength ) ,
44+ signature = state . substring ( signedTextLength ) ;
6945
7046 // assert.equal(signedText.length + signature.length, state.length);
71-
7247 if ( signer . verify ( signedText , signature ) ) {
73- payload = decodeUrlSafeBase64 ( encodedPayload ) ;
48+ payload = Buffer . from ( encodedPayload , 'base64url' ) . toString ( 'utf8' ) ;
7449 }
7550 } catch ( err ) {
7651 // nothing
0 commit comments