Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"dependencies": {
"@basis-theory/basis-theory-js": "^4.28.2",
"card-validator": "^10.0.2",
"jose": "^4.15.4",
"ramda": "^0.30.1",
"react-native-mask-input": "^1.2.3",
"react-native-url-polyfill": "^2.0.0",
Expand Down
49 changes: 49 additions & 0 deletions src/model/EncryptTokenData.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// TODO: Migrate to web-elements when we completely drop basis-theory-js
import {
CreateToken,
TokenBase,
} from '@basis-theory/basis-theory-js/types/models';
import { BTRef, InputBTRefWithDatepart } from '../BaseElementTypes';

/**
* Represents token data with element references for secure input handling.
* Used when creating tokens that contain form element references instead of raw values.
*/
type TokenDataWithRef = {
/** Key-value pairs where values are element references or date part references */
data: Record<string, BTRef | InputBTRefWithDatepart | null | undefined>;
type: TokenBase['type'];
};

type TokenData = Pick<CreateToken, 'type' | 'data'>;

/**
* Configuration for encrypting token data using public key encryption.
* Contains the token requests, public key, and key identifier.
*/
type EncryptToken = {
/**
* Token requests to encrypt - can be a single token or multiple keyed tokens
*/
tokenRequests:
| { [key: string]: TokenDataWithRef }
| TokenDataWithRef;
publicKeyPEM: string;

/** Unique identifier for the encryption key obtained from https://developers.basistheory.com/docs/api/client-keys */
keyId: string;
};

/**
* Result of token encryption operation.
* Contains the encrypted token string and its original type.
*/
type EncryptedToken = {
/** Base64-encoded encrypted token data */
encrypted: string;

/** Original token type before encryption */
type: TokenBase['type'];
};

export { EncryptToken, EncryptedToken, TokenData, TokenDataWithRef };
22 changes: 22 additions & 0 deletions src/modules/tokens.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ import {
replaceSensitiveData,
} from '../utils/dataManipulationUtils';
import { isNilOrEmpty } from '../utils/shared';
import { EncryptToken } from '../model/EncryptTokenData';
import { encryptToken } from '../tokenEncryption';

export type CreateTokenWithBtRef = Omit<CreateToken, 'data'> & {
data: Record<string, BTRef | InputBTRefWithDatepart | null | undefined>;
Expand Down Expand Up @@ -115,6 +117,25 @@ export const Tokens = (bt: BasisTheoryType) => {
}
};

const encrypt = async (
encryptRequest: EncryptToken,
) => {

console.log('encryptRequest', encryptRequest);
if (!isNilOrEmpty(_elementErrors)) {
console.log('element errors', _elementErrors);
throw new Error(
'Unable to encrypt token. Payload contains invalid values. Review elements events for more details.'
);
}

try {
return await encryptToken(encryptRequest);
} catch (error) {
console.error(error);
}
};

return {
/** @deprecated use `bt.tokens.retrieve` instead */
getById: getTokenById,
Expand All @@ -123,5 +144,6 @@ export const Tokens = (bt: BasisTheoryType) => {
update,
delete: deleteToken,
tokenize,
encrypt,
};
};
136 changes: 136 additions & 0 deletions src/tokenEncryption.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
import { CompactEncrypt, importJWK, JWK, KeyLike } from 'jose';
import { EncryptedToken, EncryptToken, TokenData, TokenDataWithRef } from './model/EncryptTokenData';
import { replaceElementRefs } from './utils/dataManipulationUtils';

const ENCRYPTION = {
KEY_TYPE: 'OKP',
CURVE: 'X25519',
ALGORITHM: 'ECDH-ES',
ENCRYPTION_ALGORITHM: 'A256GCM'
} as const;

export class EncryptValidationError extends Error {
public constructor(message: string) {
super(message);
this.name = 'EncryptValidationError';
}
}

/**
* Creates a JSON Web Encryption (JWE) object for the given payload
* @param payload - The string payload to encrypt
* @param jwk - The JSON Web Key containing key metadata
* @param key - The cryptographic key for encryption
* @returns Promise resolving to the encrypted JWE string
* @throws {Error} When encryption fails or parameters are invalid
*/
const createJWE = async (
payload: string,
jwk: JWK,
key: KeyLike | Uint8Array
): Promise<string> => {
try {
return new CompactEncrypt(
new TextEncoder().encode(payload)
)
.setProtectedHeader({ alg: ENCRYPTION.ALGORITHM, enc: ENCRYPTION.ENCRYPTION_ALGORITHM, kid: jwk.kid })
.encrypt(key);
} catch (error) {
throw new Error(
`Failed to create JWE: ${
error instanceof Error ? error.message : 'Unknown error'
}`
);
}
};

/**
* Normalizes token requests to a consistent array format
* @param tokenRequests - Token requests in either single token or object format
* @returns Array of normalized token objects
*/
const normalizeTokenRequests = (
tokenRequests: EncryptToken['tokenRequests']
): Array<TokenDataWithRef> => {
if ('type' in tokenRequests) {
return [tokenRequests as TokenDataWithRef];
}

return Object.values(tokenRequests);
};

const removePEMFormat = (publicKeyPEM: string) =>
publicKeyPEM
.replace('-----BEGIN PUBLIC KEY-----', '')
.replace('-----END PUBLIC KEY-----', '')
.replace(/[\n\r]/gu, '')
.replace(/\+/gu, '-')
.replace(/\//gu, '_')
.replace(/[=]+$/gu, '');

/**
* Encrypts token data using JSON Web Encryption (JWE)
* @param payload - The encryption payload containing token requests, public key, and key ID
* @returns Promise resolving to an array of encrypted tokens
* @throws {Error} When encryption fails or payload is invalid
*/
// TODO: To avoid duplication, add this to web-elements when we completely drop basis-theory-js
export const encryptToken = async (
payload: EncryptToken
): Promise<EncryptedToken[]> => {
if (!payload) {
throw new EncryptValidationError('Encryption payload is required');
}

if (!payload.publicKeyPEM) {
throw new EncryptValidationError('Public key PEM is required');
}

if (!payload.keyId) {
throw new EncryptValidationError('Key ID is required');
}

if (!payload.tokenRequests) {
throw new EncryptValidationError('Token requests are required');
}

try {

const jwk: JWK = {
kty: ENCRYPTION.KEY_TYPE,
crv: ENCRYPTION.CURVE,
x: removePEMFormat(payload.publicKeyPEM),
kid: payload.keyId
};

const key = await importJWK(jwk, ENCRYPTION.ALGORITHM);

const tokensWithRef: TokenDataWithRef[] = normalizeTokenRequests(payload.tokenRequests);
if (!tokensWithRef.length) {
throw new EncryptValidationError('No valid tokens found to encrypt');
}

const tokens: TokenData[] = tokensWithRef.map(token => replaceElementRefs<TokenData>(token));

return await Promise.all(
tokens.map(async (token) => {
if (!token.type) {
throw new EncryptValidationError('Token type is required');
}

const tokenPayload = JSON.stringify(token);
const encrypted = await createJWE(tokenPayload, jwk, key);

return {
encrypted,
type: token.type,
};
})
);
} catch (error) {
if (error instanceof EncryptValidationError) {
throw error;
}
throw new Error(`Failed to encrypt tokens: ${String(error)}`);
}
};
Loading