Skip to content

Commit 60c82e3

Browse files
tpaksuachyuthajoy
andauthored
Cloak client secret (#4782)
Co-authored-by: Achyuth Ajoy <[email protected]>
1 parent 2ece83b commit 60c82e3

26 files changed

+416
-19
lines changed
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Significance: minor
2+
Type: add
3+
4+
Adds encryption to the exposed `client_secret` to harden the store against card testing attacks

client/checkout/api/index.js

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
getPaymentRequestAjaxURL,
1010
buildAjaxURL,
1111
} from '../../payment-request/utils';
12+
import { decryptClientSecret } from '../utils/encryption';
1213

1314
/**
1415
* Handles generic connections to the server and Stripe.
@@ -243,7 +244,9 @@ export default class WCPayAPI {
243244
// If this is a setup intent we're not processing a platform checkout payment so we can
244245
// use the regular getStripe function.
245246
if ( isSetupIntent ) {
246-
return this.getStripe().confirmCardSetup( clientSecret );
247+
return this.getStripe().confirmCardSetup(
248+
decryptClientSecret( clientSecret )
249+
);
247250
}
248251

249252
// For platform checkout we need the capability to switch up the account ID specifically for
@@ -253,12 +256,19 @@ export default class WCPayAPI {
253256
publishableKey,
254257
locale,
255258
accountIdForIntentConfirmation
256-
).confirmCardPayment( clientSecret );
259+
).confirmCardPayment(
260+
decryptClientSecret(
261+
clientSecret,
262+
accountIdForIntentConfirmation
263+
)
264+
);
257265
}
258266

259267
// When not dealing with a setup intent or platform checkout we need to force an account
260268
// specific request in Stripe.
261-
return this.getStripe( true ).confirmCardPayment( clientSecret );
269+
return this.getStripe( true ).confirmCardPayment(
270+
decryptClientSecret( clientSecret )
271+
);
262272
};
263273

264274
const confirmAction = confirmPaymentOrSetup();
@@ -357,7 +367,9 @@ export default class WCPayAPI {
357367
}
358368

359369
return this.getStripe()
360-
.confirmCardSetup( response.data.client_secret )
370+
.confirmCardSetup(
371+
decryptClientSecret( response.data.client_secret )
372+
)
361373
.then( ( confirmedSetupIntent ) => {
362374
const { setupIntent, error } = confirmedSetupIntent;
363375
if ( error ) {
@@ -470,7 +482,7 @@ export default class WCPayAPI {
470482
'lock_timeout' === confirmPaymentResult.error.code
471483
) {
472484
const paymentIntentResult = await stripe.retrievePaymentIntent(
473-
paymentIntentSecret
485+
decryptClientSecret( paymentIntentSecret )
474486
);
475487
if (
476488
! paymentIntentResult.error &&

client/checkout/blocks/upe-fields.js

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import './style.scss';
2222
import confirmUPEPayment from './confirm-upe-payment.js';
2323
import { getConfig } from 'utils/checkout';
2424
import { getTerms } from '../utils/upe';
25+
import { decryptClientSecret } from '../utils/encryption';
2526
import { PAYMENT_METHOD_NAME_CARD, WC_STORE_CART } from '../constants.js';
2627
import enableStripeLinkPaymentMethod from 'wcpay/checkout/stripe-link';
2728
import { useDispatch, useSelect } from '@wordpress/data';
@@ -419,15 +420,16 @@ const ConsumableWCPayFields = ( { api, ...props } ) => {
419420
return null;
420421
}
421422

422-
const options = {
423-
clientSecret,
424-
appearance,
425-
fonts: fontRules,
426-
loader: 'never',
427-
};
428-
429423
return (
430-
<Elements stripe={ stripe } options={ options }>
424+
<Elements
425+
stripe={ stripe }
426+
options={ {
427+
clientSecret: decryptClientSecret( clientSecret ),
428+
appearance,
429+
fonts: fontRules,
430+
loader: 'never',
431+
} }
432+
>
431433
<WCPayUPEFields
432434
api={ api }
433435
paymentIntentId={ paymentIntentId }

client/checkout/classic/upe.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import WCPayAPI from '../api';
1313
import enqueueFraudScripts from 'fraud-scripts';
1414
import { getFontRulesFromPage, getAppearance } from '../upe-styles';
1515
import { getTerms, getCookieValue, isWCPayChosen } from '../utils/upe';
16+
import { decryptClientSecret } from '../utils/encryption';
1617
import enableStripeLinkPaymentMethod from '../stripe-link';
1718
import apiRequest from '../utils/request';
1819
import showErrorCheckout from '../utils/show-error-checkout';
@@ -237,7 +238,7 @@ jQuery( function ( $ ) {
237238
}
238239

239240
elements = api.getStripe().elements( {
240-
clientSecret,
241+
clientSecret: decryptClientSecret( clientSecret ),
241242
appearance,
242243
fonts: getFontRulesFromPage(),
243244
loader: 'never',

client/checkout/utils/encryption.js

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/**
2+
* External dependencies
3+
*/
4+
import Utf8 from 'crypto-js/enc-utf8';
5+
import AES from 'crypto-js/aes';
6+
import Pkcs7 from 'crypto-js/pad-pkcs7';
7+
import { getConfig } from 'wcpay/utils/checkout';
8+
9+
export const decryptClientSecret = function (
10+
encryptedValue,
11+
stripeAccountId = null
12+
) {
13+
if (
14+
getConfig( 'isClientEncryptionEnabled' ) &&
15+
3 < encryptedValue.length &&
16+
'pi_' !== encryptedValue.slice( 0, 3 ) &&
17+
'seti_' !== encryptedValue.slice( 0, 5 )
18+
) {
19+
stripeAccountId = stripeAccountId || getConfig( 'accountId' );
20+
return Utf8.stringify(
21+
AES.decrypt(
22+
encryptedValue,
23+
Utf8.parse( stripeAccountId.slice( 5 ) ),
24+
{
25+
iv: Utf8.parse( 'WC'.repeat( 8 ) ),
26+
padding: Pkcs7,
27+
}
28+
)
29+
);
30+
}
31+
return encryptedValue;
32+
};

client/data/settings/actions.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,12 @@ export function updateIsCardPresentEligible( isEnabled ) {
2828
return updateSettingsValues( { is_card_present_eligible: isEnabled } );
2929
}
3030

31+
export function updateIsClientSecretEncryptionEnabled( isEnabled ) {
32+
return updateSettingsValues( {
33+
is_client_secret_encryption_enabled: isEnabled,
34+
} );
35+
}
36+
3137
export function updatePaymentRequestButtonType( type ) {
3238
return updateSettingsValues( { payment_request_button_type: type } );
3339
}

client/data/settings/hooks.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,19 @@ export const useCardPresentEligible = () => {
3030
return [ isCardPresentEligible, updateIsCardPresentEligible ];
3131
};
3232

33+
export const useClientSecretEncryption = () => {
34+
const { updateIsClientSecretEncryptionEnabled } = useDispatch( STORE_NAME );
35+
36+
const isClientSecretEncryptionEnabled = useSelect( ( select ) => {
37+
return select( STORE_NAME ).getIsClientSecretEncryptionEnabled();
38+
}, [] );
39+
40+
return [
41+
isClientSecretEncryptionEnabled,
42+
updateIsClientSecretEncryptionEnabled,
43+
];
44+
};
45+
3346
export const useEnabledPaymentMethodIds = () => {
3447
const { updateEnabledPaymentMethodIds } = useDispatch( STORE_NAME );
3548

client/data/settings/selectors.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@ export const getIsWCPayEnabled = ( state ) => {
2323
return getSettings( state ).is_wcpay_enabled || false;
2424
};
2525

26+
export const getIsClientSecretEncryptionEnabled = ( state ) => {
27+
return getSettings( state ).is_client_secret_encryption_enabled || false;
28+
};
29+
2630
export const getEnabledPaymentMethodIds = ( state ) => {
2731
return getSettings( state ).enabled_payment_method_ids || EMPTY_ARR;
2832
};

client/data/settings/test/hooks.js

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
usePlatformCheckoutEnabledSettings,
1919
usePlatformCheckoutCustomMessage,
2020
usePlatformCheckoutStoreLogo,
21+
useClientSecretEncryption,
2122
} from '../hooks';
2223
import { STORE_NAME } from '../../constants';
2324

@@ -268,6 +269,39 @@ describe( 'Settings hooks tests', () => {
268269
} );
269270
} );
270271

272+
describe( 'useClientSecretEncryption()', () => {
273+
test( 'returns and updates client secret encryption settings', () => {
274+
const clientSecretEncryptionBeforeUpdate = false;
275+
const clientSecretEncryptionAfterUpdate = true;
276+
277+
actions = {
278+
updateIsClientSecretEncryptionEnabled: jest.fn(),
279+
};
280+
281+
selectors = {
282+
getIsClientSecretEncryptionEnabled: jest.fn(
283+
() => clientSecretEncryptionBeforeUpdate
284+
),
285+
};
286+
287+
const [
288+
isClientEncryptionEnabled,
289+
updateIsClientSecretEncryptionEnabled,
290+
] = useClientSecretEncryption();
291+
292+
updateIsClientSecretEncryptionEnabled(
293+
clientSecretEncryptionAfterUpdate
294+
);
295+
296+
expect( isClientEncryptionEnabled ).toEqual(
297+
clientSecretEncryptionBeforeUpdate
298+
);
299+
expect(
300+
actions.updateIsClientSecretEncryptionEnabled
301+
).toHaveBeenCalledWith( clientSecretEncryptionAfterUpdate );
302+
} );
303+
} );
304+
271305
describe( 'usePlatformCheckoutEnabledSettings()', () => {
272306
test( 'returns platform checkout setting from selector', () => {
273307
actions = {

client/data/settings/test/reducer.js

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
updateIsPlatformCheckoutEnabled,
1919
updatePlatformCheckoutCustomMessage,
2020
updatePlatformCheckoutStoreLogo,
21+
updateIsClientSecretEncryptionEnabled,
2122
} from '../actions';
2223

2324
describe( 'Settings reducer tests', () => {
@@ -503,4 +504,48 @@ describe( 'Settings reducer tests', () => {
503504
} );
504505
} );
505506
} );
507+
508+
describe( 'SET_IS_CLIENT_SECRET_ENCRYPTION_ENABLED', () => {
509+
test( 'toggle `data.is_client_secret_encryption_enabled`', () => {
510+
const oldState = {
511+
data: {
512+
is_client_secret_encryption_enabled: false,
513+
},
514+
};
515+
516+
const state = reducer(
517+
oldState,
518+
updateIsClientSecretEncryptionEnabled( true )
519+
);
520+
521+
expect( state.data.is_client_secret_encryption_enabled ).toEqual(
522+
true
523+
);
524+
} );
525+
526+
test( 'leaves other fields unchanged', () => {
527+
const oldState = {
528+
foo: 'bar',
529+
data: {
530+
is_client_secret_encryption_enabled: false,
531+
baz: 'quux',
532+
},
533+
savingError: {},
534+
};
535+
536+
const state = reducer(
537+
oldState,
538+
updateIsClientSecretEncryptionEnabled( true )
539+
);
540+
541+
expect( state ).toEqual( {
542+
foo: 'bar',
543+
data: {
544+
is_client_secret_encryption_enabled: true,
545+
baz: 'quux',
546+
},
547+
savingError: null,
548+
} );
549+
} );
550+
} );
506551
} );

0 commit comments

Comments
 (0)