Skip to content

Commit

Permalink
feat: [SIW-1093] Better handling of platform specific functions (#12)
Browse files Browse the repository at this point in the history
* feat: better handling of platform specific functions

* fix: wrong specific methods

* test: update tests

---------

Co-authored-by: Mario Perrotta <[email protected]>
  • Loading branch information
LazyAfternoons and hevelius authored May 28, 2024
1 parent 0773aee commit 8be92e9
Show file tree
Hide file tree
Showing 2 changed files with 145 additions and 73 deletions.
137 changes: 94 additions & 43 deletions src/__tests__/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ import {
getAttestation,
isAttestationServiceAvailable,
generateHardwareSignatureWithAssertion,
isPlayServicesAvailable,
prepareIntegrityToken,
requestIntegrityToken,
} from '..';

jest.mock('react-native', () => ({
Expand All @@ -13,67 +16,115 @@ jest.mock('react-native', () => ({
generateHardwareKey: jest.fn(),
getAttestation: jest.fn(),
generateHardwareSignatureWithAssertion: jest.fn(),
requestIntegrityToken: jest.fn(),
prepareIntegrityToken: jest.fn(),
isPlayServicesAvailable: jest.fn(),
},
},
Platform: {
select: jest.fn(),
},
}));

const mockPlatformiOS = () =>
(Platform.select = jest.fn().mockImplementation((options) => options.ios));

const mockPlatformAndroid = () =>
(Platform.select = jest
.fn()
.mockImplementation((options) => options.android));

describe('Test integrity check function exposed by main package', () => {
it('should be called correctly on iOS', async () => {
Platform.OS = 'ios';
describe('iOS functions', () => {
beforeAll(() => {
mockPlatformiOS();
});

it('isAttestationServiceAvailable should be called correctly', async () => {
const spy = jest.spyOn(
NativeModules.IoReactNativeIntegrity,
'isAttestationServiceAvailable'
);

await isAttestationServiceAvailable();

expect(spy).toHaveBeenCalled();
});

const spy = jest.spyOn(
NativeModules.IoReactNativeIntegrity,
'isAttestationServiceAvailable'
);
it('generateHardwareKey it should be called correctly', async () => {
const spy = jest.spyOn(
NativeModules.IoReactNativeIntegrity,
'generateHardwareKey'
);
await generateHardwareKey();
expect(spy).toHaveBeenCalled();
});

await isAttestationServiceAvailable();
it('getAttestation it should be called correctly', async () => {
const spy = jest.spyOn(
NativeModules.IoReactNativeIntegrity,
'getAttestation'
);
await getAttestation('challenge', 'hardwareKeyTag');
expect(spy).toHaveBeenCalledWith('challenge', 'hardwareKeyTag');
});

expect(spy).toHaveBeenCalled();
it('generateHardwareSignatureWithAssertion it should be called correctly', async () => {
const spy = jest.spyOn(
NativeModules.IoReactNativeIntegrity,
'generateHardwareSignatureWithAssertion'
);
await generateHardwareSignatureWithAssertion(
'clientData',
'hardwareKeyTag'
);
expect(spy).toHaveBeenCalledWith('clientData', 'hardwareKeyTag');
});
});

it('should be called correctly on Android', async () => {
Platform.OS = 'android';
describe('Android functions', () => {
beforeAll(() => {
mockPlatformAndroid();
});

const spy = jest.spyOn(
NativeModules.IoReactNativeIntegrity,
'isAttestationServiceAvailable'
);
it('isPlayServicesAvailable should be called correctly', async () => {
const spy = jest.spyOn(
NativeModules.IoReactNativeIntegrity,
'isPlayServicesAvailable'
);

await isAttestationServiceAvailable();
await isPlayServicesAvailable();

expect(spy).toHaveBeenCalled();
});
expect(spy).toHaveBeenCalled();
});

it('generateHardwareKey it should be called correctly', async () => {
const spy = jest.spyOn(
NativeModules.IoReactNativeIntegrity,
'generateHardwareKey'
);
await generateHardwareKey();
expect(spy).toHaveBeenCalled();
});
it('prepareIntegrityToken should be called correctly', async () => {
const spy = jest.spyOn(
NativeModules.IoReactNativeIntegrity,
'prepareIntegrityToken'
);

it('getAttestation it should be called correctly', async () => {
const spy = jest.spyOn(
NativeModules.IoReactNativeIntegrity,
'getAttestation'
);
await getAttestation('challenge', 'hardwareKeyTag');
expect(spy).toHaveBeenCalledWith('challenge', 'hardwareKeyTag');
});
await prepareIntegrityToken('cloudProjectNumber');

expect(spy).toHaveBeenCalledWith('cloudProjectNumber');
});

it('requestIntegrityToken should be called correctly', async () => {
const spy = jest.spyOn(
NativeModules.IoReactNativeIntegrity,
'requestIntegrityToken'
);
await requestIntegrityToken('requestHash');
expect(spy).toHaveBeenCalledWith('requestHash');
});

it('generateHardwareSignatureWithAssertion it should be called correctly', async () => {
const spy = jest.spyOn(
NativeModules.IoReactNativeIntegrity,
'generateHardwareSignatureWithAssertion'
);
await generateHardwareSignatureWithAssertion(
'clientData',
'hardwareKeyTag'
);
expect(spy).toHaveBeenCalledWith('clientData', 'hardwareKeyTag');
it('getAttestation it should be called correctly', async () => {
const spy = jest.spyOn(
NativeModules.IoReactNativeIntegrity,
'getAttestation'
);
await getAttestation('challenge', 'hardwareKeyTag');
expect(spy).toHaveBeenCalledWith('challenge', 'hardwareKeyTag');
});
});
});
81 changes: 51 additions & 30 deletions src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,12 @@ type IntegrityErrorCodesIOS =
| 'CLIENT_DATA_ENCODING_ERROR'
| 'GENERATION_ASSERTION_FAILED';

type IntegrityErrorCodesCommon = 'PLATFORM_NOT_SUPPORTED';

export type IntegrityErrorCodes =
| IntegrityErrorCodesIOS
| IntegrityErrorCodesAndroid;
| IntegrityErrorCodesAndroid
| IntegrityErrorCodesCommon;

/**
* Error type returned by a rejected promise.
Expand All @@ -41,6 +44,14 @@ export type IntegrityError = {
userInfo: Record<string, string>;
};

/**
* Error when the platform is not supported.
*/
const IntegrityErrorUnsupportedError: IntegrityError = {
message: 'PLATFORM_NOT_SUPPORTED',
userInfo: {},
};

const LINKING_ERROR =
`The package '@pagopa/io-react-native-integrity' doesn't seem to be linked. Make sure: \n\n` +
Platform.select({ ios: "- You have run 'pod install'\n", default: '' }) +
Expand All @@ -59,6 +70,7 @@ const IoReactNativeIntegrity = NativeModules.IoReactNativeIntegrity
);

/**
* iOS ONLY
* This function checks if the attestation service is available on the device.
*
* If it is not possible to retrive the key, the promise is rejected providing an
Expand All @@ -67,12 +79,14 @@ const IoReactNativeIntegrity = NativeModules.IoReactNativeIntegrity
* @returns a promise that resolves to a boolean.
*/
export function isAttestationServiceAvailable(): Promise<boolean> {
return Platform.OS === 'ios'
? IoReactNativeIntegrity.isAttestationServiceAvailable()
: Promise.resolve(false); // TODO: implement for Android
return Platform.select({
ios: () => IoReactNativeIntegrity.isAttestationServiceAvailable(),
default: () => Promise.reject(IntegrityErrorUnsupportedError),
})();
}

/**
* iOS ONLY
* This function generates a hardware key that can be used into the attestation process.
*
* If it is not possible to retrive the key, the promise is rejected providing an
Expand All @@ -81,10 +95,14 @@ export function isAttestationServiceAvailable(): Promise<boolean> {
* @returns a promise that resolves to a string.
*/
export function generateHardwareKey(): Promise<string> {
return IoReactNativeIntegrity.generateHardwareKey();
return Platform.select({
ios: () => IoReactNativeIntegrity.generateHardwareKey(),
default: () => Promise.reject(IntegrityErrorUnsupportedError),
})();
}

/**
* iOS ONLY
* This function generates an attestation for the given challenge and hardware key.
*
* If it is not possible to retrive the attestation, the promise is rejected providing an
Expand All @@ -98,10 +116,16 @@ export function getAttestation(
challenge: string,
hardwareKeyTag: string
): Promise<string> {
return IoReactNativeIntegrity.getAttestation(challenge, hardwareKeyTag);
return Platform.select({
ios: () => IoReactNativeIntegrity.getAttestation(challenge, hardwareKeyTag),
android: () =>
IoReactNativeIntegrity.getAttestation(challenge, hardwareKeyTag),
default: () => Promise.reject(IntegrityErrorUnsupportedError),
})();
}

/**
* iOS ONLY
* This function generates a signature for the given client data given an hardware key.
*
* If it is not possible to retrive the signature, the promise is rejected providing an
Expand All @@ -115,32 +139,26 @@ export function generateHardwareSignatureWithAssertion(
clientData: string,
hardwareKeyTag: string
): Promise<string> {
return IoReactNativeIntegrity.generateHardwareSignatureWithAssertion(
clientData,
hardwareKeyTag
);
return Platform.select({
ios: () =>
IoReactNativeIntegrity.generateHardwareSignatureWithAssertion(
clientData,
hardwareKeyTag
),
default: () => Promise.reject(IntegrityErrorUnsupportedError),
})();
}

/**
* Checks whether the current platform is Android or not.
* @returns true if the current platform is Android, false otherwise.
*/
const isAndroid = () => Platform.OS === 'android';

/**
* Error message for functions available only on Android.
*/
const NOT_ANDROID_ERROR = 'This function is available only on Android';

/**
* ANDROID ONLY
* Checks whether Google Play Services is available on the device or not.
* @return a promise resolved to true if Google Play Services is available, to false otherwise.
*/
export function isPlayServicesAvailable(): Promise<boolean> {
return isAndroid()
? IoReactNativeIntegrity.isPlayServicesAvailable()
: Promise.resolve(false);
return Platform.select({
android: () => IoReactNativeIntegrity.isPlayServicesAvailable(),
default: () => Promise.reject(IntegrityErrorUnsupportedError),
})();
}

/**
Expand All @@ -160,9 +178,11 @@ export function isPlayServicesAvailable(): Promise<boolean> {
export function prepareIntegrityToken(
cloudProjectNumber: string
): Promise<void> {
return isAndroid()
? IoReactNativeIntegrity.prepareIntegrityToken(cloudProjectNumber)
: Promise.reject(NOT_ANDROID_ERROR);
return Platform.select({
android: () =>
IoReactNativeIntegrity.prepareIntegrityToken(cloudProjectNumber),
default: () => Promise.reject(IntegrityErrorUnsupportedError),
})();
}

/**
Expand All @@ -178,7 +198,8 @@ export function prepareIntegrityToken(
* - The {@link prepareIntegrityToken} function hasn't been called previously.
*/
export function requestIntegrityToken(requestHash?: string): Promise<string> {
return isAndroid()
? IoReactNativeIntegrity.requestIntegrityToken(requestHash)
: Promise.reject(NOT_ANDROID_ERROR);
return Platform.select({
android: () => IoReactNativeIntegrity.requestIntegrityToken(requestHash),
default: () => Promise.reject(IntegrityErrorUnsupportedError),
})();
}

0 comments on commit 8be92e9

Please sign in to comment.