Skip to content

Commit

Permalink
feat(service-desk): WebApiCrypto encryption (#14768)
Browse files Browse the repository at this point in the history
* Create encrypt and decrypt actions to cypher text using the Web Api Crypto library

* merged with main

* fix test

* fix code so test will run on node rather then jsdom

* fix build after nullable unmask/mask

* fix pr comments

* chore: nx format:write update dirty files

* fix encryption to be url friendly

* chore: nx format:write update dirty files

---------

Co-authored-by: Sævar Már Atlason <[email protected]>
Co-authored-by: andes-it <[email protected]>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
  • Loading branch information
4 people authored Jun 3, 2024
1 parent a831ebe commit af29933
Show file tree
Hide file tree
Showing 9 changed files with 216 additions and 89 deletions.
16 changes: 10 additions & 6 deletions libs/api/domains/national-registry/src/lib/v3/mapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,12 @@ import * as kennitala from 'kennitala'
import { maskString, isDefined } from '@island.is/shared/utils'
import { FamilyChild, User } from './types'

export const formatPersonDiscriminated = (
export const formatPersonDiscriminated = async (
individual?: EinstaklingurDTOAllt | null,
nationalId?: string,
useFakeData?: boolean,
): PersonV3 | null => {
const person = formatPerson(individual, nationalId)
): Promise<PersonV3 | null> => {
const person = await formatPerson(individual, nationalId)
if (!person) {
return null
}
Expand Down Expand Up @@ -64,14 +64,18 @@ export const formatChildCustody = (
}
}

export const formatPerson = (
export const formatPerson = async (
individual?: EinstaklingurDTOAllt | null,
nationalId?: string,
): Person | null => {
): Promise<Person | null> => {
if (individual === null || !individual?.kennitala || !individual?.nafn) {
return null
}

const maskedNationalId = nationalId
? await maskString(individual.kennitala, nationalId)
: null

return {
nationalId: individual.kennitala,
fullName: individual.nafn,
Expand All @@ -84,7 +88,7 @@ export const formatPerson = (
),
...(nationalId &&
individual.kennitala && {
baseId: maskString(individual.kennitala, nationalId),
baseId: maskedNationalId,
}),

//DEPRECATION LINE -- below shall be removed
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export const userLoader: WrappedLoaderFn = ({ client, userInfo }) => {
if (!nationalId) throw new Error('User not found')

const unMaskedNationalId =
unmaskString(nationalId, userInfo.profile.nationalId) ?? ''
(await unmaskString(nationalId, userInfo.profile.nationalId)) ?? ''

const res = await client.query<
GetUserProfileByNationalIdQuery,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
import { z } from 'zod'
import { redirect } from 'react-router-dom'
import { isEmail } from 'class-validator'
import * as kennitala from 'kennitala'
import { parsePhoneNumber } from 'libphonenumber-js'

import {
RawRouterActionResponse,
Expand All @@ -13,7 +10,7 @@ import {
validateFormData,
ValidateFormDataResult,
} from '@island.is/react-spa/shared'
import { isSearchTermValid, maskString } from '@island.is/shared/utils'
import { maskString, isSearchTermValid } from '@island.is/shared/utils'

import {
GetPaginatedUserProfilesDocument,
Expand Down Expand Up @@ -88,10 +85,10 @@ export const UsersAction: WrappedActionFn =
replaceParams({
href: ServiceDeskPaths.User,
params: {
nationalId: maskString(
nationalId: (await maskString(
respData[0].nationalId,
userInfo.profile.nationalId,
) as string,
)) as string,
},
}),
)
Expand Down
6 changes: 3 additions & 3 deletions libs/portals/admin/service-desk/src/screens/Users/Users.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -97,16 +97,16 @@ const Users = () => {
variant="text"
icon="arrowForward"
size="small"
onClick={() =>
onClick={async () =>
navigate(
replaceParams({
href: ServiceDeskPaths.User,
params: {
nationalId:
maskString(
(await maskString(
nationalId,
userInfo?.profile?.nationalId ?? '',
) ?? '',
)) ?? '',
},
}),
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { spmm } from '../../lib/messages'
import { unmaskString } from '@island.is/shared/utils'
import { Problem } from '@island.is/react-spa/shared'
import { useNationalRegistryBioChildQuery } from './BioChild.generated'
import { useEffect, useState } from 'react'

type UseParams = {
baseId: string
Expand All @@ -33,11 +34,29 @@ const BioChild = () => {
const { formatMessage } = useLocale()
const userInfo = useUserInfo()
const { baseId } = useParams() as UseParams
const [unmaskedBaseId, setUnmaskedBaseId] = useState<string | null>(null)

useEffect(() => {
const decrypt = async () => {
try {
const decrypted = await unmaskString(
baseId,
userInfo.profile.nationalId,
)
setUnmaskedBaseId(decrypted ?? null)
} catch (e) {
console.error('Failed to decrypt baseId', e)
}
}

decrypt()
}, [baseId, userInfo])

const { data, loading, error } = useNationalRegistryBioChildQuery({
variables: {
childNationalId: unmaskString(baseId, userInfo.profile.nationalId),
childNationalId: unmaskedBaseId,
},
skip: !unmaskedBaseId,
})

const child = data?.nationalRegistryPerson?.biologicalChildren?.[0].details
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { useUserInfo } from '@island.is/auth/react'
import { defineMessage } from 'react-intl'
import {
Box,
Button,
Expand All @@ -11,21 +12,21 @@ import {
import { useLocale, useNamespaces } from '@island.is/localization'
import { Problem } from '@island.is/react-spa/shared'
import {
formatNationalId,
IntroHeader,
LinkButton,
m,
THJODSKRA_SLUG,
UserInfoLine,
formatNationalId,
m,
} from '@island.is/service-portal/core'
import { unmaskString } from '@island.is/shared/utils'
import { defineMessage } from 'react-intl'
import { useParams } from 'react-router-dom'
import { TwoColumnUserInfoLine } from '../../components/TwoColumnUserInfoLine/TwoColumnUserInfoLine'
import { formatNameBreaks } from '../../helpers/formatting'
import { natRegGenderMessageDescriptorRecord } from '../../helpers/localizationHelpers'
import { spmm, urls } from '../../lib/messages'
import { useNationalRegistryChildCustodyQuery } from './ChildCustody.generated'
import { useEffect, useState } from 'react'

type UseParams = {
baseId: string
Expand All @@ -36,11 +37,29 @@ const ChildCustody = () => {
const { formatMessage } = useLocale()
const userInfo = useUserInfo()
const { baseId } = useParams() as UseParams
const [unmaskedBaseId, setUnmaskedBaseId] = useState<string | null>(null)

useEffect(() => {
const decrypt = async () => {
try {
const decrypted = await unmaskString(
baseId,
userInfo.profile.nationalId,
)
setUnmaskedBaseId(decrypted)
} catch (error) {
console.error('Error encrypting text:', error)
}
}

decrypt()
}, [baseId, userInfo])

const { data, loading, error } = useNationalRegistryChildCustodyQuery({
variables: {
childNationalId: unmaskString(baseId, userInfo.profile.nationalId),
childNationalId: unmaskedBaseId,
},
skip: !unmaskedBaseId,
})

const child = data?.nationalRegistryPerson?.childCustody?.[0]?.details
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,14 @@ import { spmm } from '../../lib/messages'
import { maskString } from '@island.is/shared/utils'
import { useUserInfoOverviewQuery } from './UserInfoOverview.generated'
import { Problem } from '@island.is/react-spa/shared'
import { useEffect, useState } from 'react'

const UserInfoOverview = () => {
useNamespaces('sp.family')
const { formatMessage } = useLocale()
const userInfo = useUserInfo()
const [childCards, setChildCards] = useState<JSX.Element[]>([])
const [bioChildrenCards, setBioChildrenCards] = useState<JSX.Element[]>([])

const { data, error, loading } = useUserInfoOverviewQuery()

Expand All @@ -30,6 +33,57 @@ const UserInfoOverview = () => {
(child) => !childCustody?.some((c) => c.nationalId === child.nationalId),
)

useEffect(() => {
const fetchChildCustodyData = async () => {
try {
if (childCustody) {
const childrenData = await Promise.all(
childCustody.map(async (child) => {
const baseId = await maskString(
child.nationalId,
userInfo.profile.nationalId,
)
return (
<FamilyMemberCard
key={child.nationalId}
title={child.fullName || ''}
nationalId={child.nationalId}
baseId={baseId ?? ''}
familyRelation="custody"
/>
)
}),
)
setChildCards(childrenData)
}
if (bioChildren) {
const bioChildrenData = await Promise.all(
bioChildren.map(async (child) => {
const baseId = await maskString(
child.nationalId,
userInfo.profile.nationalId,
)
return (
<FamilyMemberCard
key={child.nationalId}
title={child.fullName || ''}
nationalId={child.nationalId}
baseId={baseId ?? ''}
familyRelation="bio-child"
/>
)
}),
)
setBioChildrenCards(bioChildrenData)
}
} catch (e) {
console.error('Failed setting childCards', e)
}
}

fetchChildCustodyData()
}, [data, userInfo.profile.nationalId])

return (
<>
<IntroHeader
Expand Down Expand Up @@ -71,28 +125,8 @@ const UserInfoOverview = () => {
familyRelation="spouse"
/>
)}
{childCustody?.map((child) => (
<FamilyMemberCard
key={child.nationalId}
title={child.fullName || ''}
nationalId={child.nationalId}
baseId={
maskString(child.nationalId, userInfo.profile.nationalId) ?? ''
}
familyRelation="custody"
/>
))}
{bioChildren?.map((child) => (
<FamilyMemberCard
key={child.nationalId}
title={child.fullName || ''}
nationalId={child.nationalId}
baseId={
maskString(child.nationalId, userInfo.profile.nationalId) ?? ''
}
familyRelation="bio-child"
/>
))}
{childCards}
{bioChildrenCards}
<FootNote serviceProviderSlug={THJODSKRA_SLUG} />
</Stack>
)}
Expand Down
31 changes: 18 additions & 13 deletions libs/shared/utils/src/lib/simpleEncryption.spec.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,34 @@
/**
* @jest-environment node
*/

import { maskString, unmaskString } from './simpleEncryption'

const originalText = 'Original Jest Text!'
const secretKey = 'not-really-secret-key'

describe('Encryption and Decryption Functions', () => {
test('Encrypt and decrypt a string successfully', () => {
const encrypted = maskString(originalText, secretKey)
test('Encrypt and decrypt a string successfully', async () => {
const encrypted = await maskString(originalText, secretKey)

// Check for successful encryption
expect(encrypted).not.toBe(originalText)
expect(encrypted).not.toBeNull()

// If null check succeeds, we can safely cast to string for the unmasking test
const textToDecrypt = encrypted as string

// Check for successful decryption
if (encrypted !== null) {
const decrypted = unmaskString(encrypted, secretKey)
expect(decrypted).toBe(originalText)
expect(encrypted).not.toBe(originalText)
} else {
// Fail the test explicitly if encryption failed
fail('Encryption failed')
}
const decrypted = await unmaskString(textToDecrypt, secretKey)
expect(decrypted).toBe(originalText)
expect(encrypted).not.toBe(originalText)
})

test('Return null in case of decryption failure', () => {
test('Return null in case of decryption failure', async () => {
// Example: testing decryption failure
const decryptedFailure = unmaskString('invalid-encrypted-text', secretKey)
const decryptedFailure = await unmaskString(
'invalid-encrypted-text',
secretKey,
)
expect(decryptedFailure).toBeNull()
})
})
Loading

0 comments on commit af29933

Please sign in to comment.