Merge branch 'feat/inda-383-daily-stat' into 'main'
[ProtonMail-WebClient.git] / packages / docs-core / lib / Services / Encryption / EncryptionService.ts
blob4951bd7af7c950d1d167c113fa65cbad29fd42e6
1 import type {
2   MaybeArray,
3   PrivateKeyReference,
4   PublicKeyReference,
5   SessionKey,
6   VERIFICATION_STATUS,
7 } from '@proton/crypto'
8 import { CryptoProxy } from '@proton/crypto'
9 import { SignedPlaintextContent } from '@proton/docs-proto'
10 import {
11   encryptDataWith16ByteIV as gcmEncryptWith16ByteIV,
12   decryptData as gcmDecrypt,
13 } from '@proton/crypto/lib/subtle/aesGcm'
14 import mergeUint8Arrays from '@proton/utils/mergeUint8Arrays'
15 import { stringToUtf8Array } from '@proton/crypto/lib/utils'
16 import type { EncryptionContext } from './EncryptionContext'
17 import { deriveGcmKey } from '../../Crypto/deriveGcmKey'
18 import { HKDF_SALT_SIZE } from '../../Crypto/Constants'
19 import { Result } from '@proton/docs-shared'
20 import type { DriveCompatWrapper } from '@proton/drive-store/lib/DriveCompatWrapper'
22 export class EncryptionService<C extends EncryptionContext> {
23   constructor(
24     private context: C,
25     private driveCompat: DriveCompatWrapper,
26   ) {
27     this.context = context
28   }
30   private getContext(associatedData: string) {
31     return `docs.${this.context}.${associatedData}`
32   }
34   public async signAndEncryptData(
35     data: Uint8Array,
36     associatedData: string,
37     sessionKey: SessionKey,
38     signingKey: PrivateKeyReference,
39   ): Promise<Result<Uint8Array>> {
40     try {
41       const contextString = this.getContext(associatedData)
42       const contextBytes = stringToUtf8Array(contextString)
43       const signature = await CryptoProxy.signMessage({
44         binaryData: data,
45         signingKeys: signingKey,
46         context: { value: contextString, critical: true },
47         detached: true,
48         format: 'binary',
49       })
50       const contentToEncrypt = new SignedPlaintextContent({
51         content: data,
52         signature,
53       })
55       const hkdfSalt = crypto.getRandomValues(new Uint8Array(HKDF_SALT_SIZE))
56       const key = await deriveGcmKey(sessionKey, hkdfSalt, contextBytes)
57       const ciphertext = await gcmEncryptWith16ByteIV(key, contentToEncrypt.serializeBinary(), contextBytes)
58       return Result.ok(mergeUint8Arrays([hkdfSalt, ciphertext]))
59     } catch (error) {
60       return Result.fail(`Failed to sign and encrypt data ${error}`)
61     }
62   }
64   public async encryptAnonymousData(
65     data: Uint8Array,
66     associatedData: string,
67     sessionKey: SessionKey,
68   ): Promise<Result<Uint8Array>> {
69     try {
70       const contextString = this.getContext(associatedData)
71       const contextBytes = stringToUtf8Array(contextString)
72       const contentToEncrypt = new SignedPlaintextContent({
73         content: data,
74       })
76       const hkdfSalt = crypto.getRandomValues(new Uint8Array(HKDF_SALT_SIZE))
77       const key = await deriveGcmKey(sessionKey, hkdfSalt, contextBytes)
78       const ciphertext = await gcmEncryptWith16ByteIV(key, contentToEncrypt.serializeBinary(), contextBytes)
79       return Result.ok(mergeUint8Arrays([hkdfSalt, ciphertext]))
80     } catch (error) {
81       return Result.fail(`Failed to sign and encrypt data ${error}`)
82     }
83   }
85   public async decryptData(
86     encryptedData: Uint8Array,
87     associatedData: string,
88     sessionKey: SessionKey,
89   ): Promise<Result<SignedPlaintextContent>> {
90     try {
91       const contextString = this.getContext(associatedData)
92       const contextBytes = stringToUtf8Array(contextString)
93       const hkdfSalt = encryptedData.subarray(0, HKDF_SALT_SIZE)
94       const ciphertext = encryptedData.subarray(HKDF_SALT_SIZE)
95       const key = await deriveGcmKey(sessionKey, hkdfSalt, contextBytes)
96       const decryptedData = await gcmDecrypt(key, ciphertext, contextBytes, true)
98       return Result.ok(SignedPlaintextContent.deserializeBinary(decryptedData))
99     } catch (error) {
100       return Result.fail(`Failed to decrypt data ${error}`)
101     }
102   }
104   async verifyData(
105     data: Uint8Array,
106     signature: Uint8Array,
107     associatedData: string,
108     verificationKeys: MaybeArray<PublicKeyReference>,
109   ): Promise<Result<VERIFICATION_STATUS>> {
110     try {
111       const contextString = this.getContext(associatedData)
112       const { verified } = await CryptoProxy.verifyMessage({
113         binaryData: data,
114         binarySignature: signature,
115         verificationKeys,
116         context: { value: contextString, required: true },
117         format: 'binary',
118       })
120       return Result.ok(verified)
121     } catch (error) {
122       return Result.fail(`Failed to verify data ${error}`)
123     }
124   }
126   async getVerificationKey(email: string): Promise<Result<PublicKeyReference[]>> {
127     if (!this.driveCompat.userCompat) {
128       return Result.fail('User compat not available')
129     }
131     try {
132       const value = await this.driveCompat.userCompat.getVerificationKey(email)
134       return Result.ok(value)
135     } catch (error) {
136       return Result.fail(`Failed to get verification key ${error}`)
137     }
138   }