Skip to content

Commit e0ebb57

Browse files
committed
feat: Validate metadata signature
Signed-off-by: Louis Chemineau <[email protected]>
1 parent 37551bf commit e0ebb57

File tree

5 files changed

+102
-12
lines changed

5 files changed

+102
-12
lines changed

__tests__/api-mock.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,22 @@
66
import { afterAll, afterEach, beforeAll } from 'vitest'
77
import { setupServer } from 'msw/node'
88
import { http, HttpResponse } from 'msw'
9-
import { rootFolderMetadata, serverPublicKey, subfolderMetadata } from './consts.spec'
9+
import { rootFolderMetadata, rootFolderMetadataSignature, serverPublicKey, subfolderMetadata, subFolderMetadataSignature } from './consts.spec'
1010

1111
export const restHandlers = [
1212
http.get('http://nextcloud.local//ocs/v2.php/apps/end_to_end_encryption/api/v2/server-key', () => {
1313
return HttpResponse.json({ ocs: { data: { 'public-key': serverPublicKey }}})
1414
}),
1515
http.get('http://nextcloud.local//ocs/v2.php/apps/end_to_end_encryption/api/v2/meta-data/89', () => {
16-
return HttpResponse.json({ ocs: { data: { 'meta-data': JSON.stringify(rootFolderMetadata) }}})
16+
const response = HttpResponse.json({ ocs: { data: { 'meta-data': JSON.stringify(rootFolderMetadata) }}})
17+
response.headers.set('x-nc-e2ee-signature', rootFolderMetadataSignature)
18+
return response
1719
}),
1820
http.get('http://nextcloud.local//ocs/v2.php/apps/end_to_end_encryption/api/v2/meta-data/266', () => {
19-
return HttpResponse.json({ ocs: { data: { 'meta-data': JSON.stringify(subfolderMetadata) }}})
21+
const response = HttpResponse.json({ ocs: { data: { 'meta-data': JSON.stringify(subfolderMetadata) }}})
22+
response.headers.set('x-nc-e2ee-signature', subFolderMetadataSignature)
23+
return response
24+
}),
2025
}),
2126
]
2227

__tests__/consts.spec.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,7 @@ export const rootFolderMetadata: Metadata = {
4242
}
4343
],
4444
version: "2.0"
45-
}
46-
45+
}
4746

4847
export const subfolderMetadata: Metadata = {
4948
metadata: {
@@ -100,6 +99,9 @@ export const subfolderMetadataInfo: MetadataInfo = {
10099
folders: {},
101100
}
102101

102+
export const rootFolderMetadataSignature = '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'
103+
export const subFolderMetadataSignature = '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'
104+
103105
export const encryptedFileContent = 'O13d2Y5O7qYDTerGfZyRwHKWcEktQQiJBm5rWzY='
104106

105107
export const propFindResponse = `<?xml version="1.0"?>

src/models.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ export type Metadata = {
1818
// The metadata-key is encrypted with RSA/ECB/OAEPWithSHA-256AndMGF1Padding
1919
encryptedMetadataKey: string, // Base64 encoded. Example:: "KS9P5Et+i94PAdpTtR9pyyuTlV6/3e3E/Zzwu8ua1j/e6uHUfQDxpXsksgX95Q/Hin0caoYfwwyWVs2/wtdkHttBdjywzcNfz5yDblrdKAYoyeuCavNatA3OuFDJVcMiisiskD6GMz6o3V21ZqpHwTry05dv4jZMs88lzTOLeDJ7bmmv5Pjyfbg8lxk6oW85LJkUku3+szv+kz+as18Pk+Oe1MylLP+Zktw+1Pckem32h19MacefZI/tkZLmdmjPtKNQGqlefeTXHKnIOzykdPjBG9CJ7zS0MPN7nv0ZgXeSoEi6fUHwkzmg8GxGSjLoL6L7BhLxw7Z8YWZ1MAYyCA=="
2020
}[],
21+
filedrop?: Record<string, FileDropEntry>,
2122
version: '2.0',
2223
}
2324

@@ -41,3 +42,15 @@ export type PrivateKeyInfo = {
4142
iv: Uint8Array,
4243
salt: Uint8Array,
4344
}
45+
46+
type FileDropEntry = {
47+
ciphertext: string, // encrypted metadata (AES/GCM/NoPadding, 128 bit key size) of folder (see below for the plaintext structure). first gzipped, then encrypted, then base64 encoded."
48+
nonce: string,
49+
authenticationTag: string,
50+
users: [
51+
{
52+
userId: string
53+
encryptedFiledropKey: string, // The metadata-key is encrypted with RSA/ECB/OAEPWithSHA-256AndMGF1Padding
54+
}
55+
],
56+
}

src/services/api.ts

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ import axios from '@nextcloud/axios'
99
import type { OCSResponse } from '@nextcloud/typings/ocs'
1010

1111
import type { Metadata, PrivateKeyInfo } from '../models.ts'
12-
import { base64ToBuffer, pemToBuffer } from './utils.ts'
13-
import { loadServerPublicKey, verifyCertificateSignature } from './crypto.ts'
12+
import { base64ToBuffer, pemToBuffer, stringToBuffer } from './utils.ts'
13+
import { loadServerPublicKey, verifyCertificateSignature, verifyCMSSignature } from './crypto.ts'
1414

1515
// API: https://github.com/nextcloud/end_to_end_encryption/blob/master/doc/api.md
1616

@@ -43,13 +43,34 @@ export async function getMetadata(fileId: string, serverPublicKey: CryptoKey): P
4343

4444
const metadata = JSON.parse(response.data.ocs.data['meta-data']) as Metadata
4545

46+
verifyMetadataSignature(metadata, response.headers['x-nc-e2ee-signature'])
47+
4648
if (metadata.users !== undefined) {
4749
await verifyUserCertificates(metadata, serverPublicKey)
4850
}
4951

5052
return metadata
5153
}
5254

55+
async function verifyMetadataSignature(metadata: Metadata, signature: string): Promise<void> {
56+
const signedData = JSON.stringify(metadata, (key, value) => {
57+
if (key === 'filedrop') {
58+
return undefined
59+
}
60+
return value
61+
})
62+
63+
const verificationResult = await verifyCMSSignature(
64+
stringToBuffer(btoa(signedData)),
65+
base64ToBuffer(signature),
66+
metadata.users ?? [],
67+
)
68+
69+
if (!verificationResult) {
70+
throw new Error('Metadata signature verification failed')
71+
}
72+
}
73+
5374
async function verifyUserCertificates(metadata: Metadata, serverPublicKey: CryptoKey): Promise<void> {
5475
if (metadata.users === undefined) {
5576
throw new Error('Cannot verify certificates of subfolders metadata')
@@ -71,5 +92,6 @@ export async function getServerPublicKey(): Promise<CryptoKey> {
7192
generateOcsUrl(Url.ServerKey),
7293
{ headers: { 'X-E2EE-SUPPORTED': 'true' } },
7394
)
95+
7496
return await loadServerPublicKey(pemToBuffer(response.data.ocs.data['public-key']))
7597
}

src/services/crypto.ts

Lines changed: 53 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -99,12 +99,60 @@ export async function sha256Hash(buffer: Uint8Array): Promise<string> {
9999
export async function verifyCertificateSignature(certificate: string, publicKey: CryptoKey): Promise<boolean> {
100100
let cert = new x509.X509Certificate(certificate)
101101

102-
const verificationResult = await self.crypto.subtle.verify(
103-
cert.signatureAlgorithm,
104-
publicKey,
105-
cert.signature,
106-
new Uint8Array(cert.tbs),
102+
return cert.verify({ publicKey }, getPatchedCrypto())
103+
}
104+
105+
export async function verifyCMSSignature(signedData: Uint8Array, cmsBuffer: Uint8Array, users: {userId: string, certificate: string}[]): Promise<boolean> {
106+
// Parse the CMS buffer
107+
const cmsContent = ContentInfo.fromBER(cmsBuffer)
108+
const originalSignedData = new SignedData({ schema: cmsContent.content });
109+
110+
// Get the signer certificate from the users array
111+
const commonNameOID = '2.5.4.3'
112+
const signerInfo = originalSignedData.signerInfos[0]
113+
const signerUserId = signerInfo.sid.issuer.typesAndValues.find(({type}) => type === commonNameOID).value.valueBlock.value
114+
const signer = users.find(({userId}) => userId === signerUserId)
115+
if (signer === undefined) {
116+
throw new Error('Signer not found in the users array')
117+
}
118+
119+
const asn1Cert = fromBER(pemToBuffer(signer.certificate))
120+
const certificate = new Certificate({ schema: asn1Cert.result })
121+
122+
const verificationResult = await originalSignedData.verify(
123+
{
124+
signer: 0,
125+
trustedCerts: [certificate],
126+
data: signedData as unknown as ArrayBuffer,
127+
checkChain: true,
128+
},
129+
getPatchedCryptoEngine()
107130
)
108131

109132
return verificationResult
133+
110134
}
135+
136+
class CustomCryptoEngine extends pkijs.CryptoEngine {
137+
verify(algorithm: globalThis.AlgorithmIdentifier | RsaPssParams | EcdsaParams, key: CryptoKey, signature: BufferSource, data: ArrayBuffer): Promise<boolean> {
138+
return super.verify(algorithm, key, signature, new Uint8Array(data))
139+
}
140+
}
141+
142+
// Return a patched crypto engine because pkijs' default engine does not give the correct data type to the subtle.verify method
143+
function getPatchedCryptoEngine() {
144+
return new CustomCryptoEngine({ crypto: self.crypto })
145+
}
146+
147+
// Return a patched crypto because x509's default does not give the correct data type to the subtle.verify method
148+
function getPatchedCrypto(): Crypto {
149+
return {
150+
...self.crypto,
151+
subtle: {
152+
...self.crypto.subtle,
153+
async verify(algorithm: globalThis.AlgorithmIdentifier | RsaPssParams | EcdsaParams, key: CryptoKey, signature: ArrayBuffer, data: ArrayBuffer): Promise<boolean> {
154+
return self.crypto.subtle.verify(algorithm, key, new Uint8Array(signature), new Uint8Array(data))
155+
}
156+
},
157+
}
158+
}

0 commit comments

Comments
 (0)