diff --git a/docs/API.md b/docs/API.md index f9093b62a..afbfb3276 100644 --- a/docs/API.md +++ b/docs/API.md @@ -203,7 +203,15 @@ returns amount of tokens to transfer to the provider account returns encrypted blob -#### Request +#### Query Parameters + +| name | type | required | description | +| --------------- | ------ | -------- | ------------------------------------------------------- | +| nonce | string | v | is required to verify a request paired with a signature | +| consumerAddress | string | v | consumer address | +| signature | string | v | signed message based on ` nonce` | + +#### Request body ``` string @@ -217,6 +225,44 @@ string --- +## EncryptFile + +### `HTTP` POST /api/services/encryptFile + +#### Description + +returns encrypted file + +#### Query Parameters + +| name | type | required | description | +| --------------- | ------ | -------- | ------------------------------------------------------- | +| nonce | string | v | is required to verify a request paired with a signature | +| consumerAddress | string | v | consumer address | +| signature | string | v | signed message based on ` nonce` | + +#### Request body + +if Content-Type = 'application/json' + +``` +BaseFileObject +``` + +if Content-Type = 'application/octet-stream' || 'multipart/form-data' + +``` +FileContent(bytes) +``` + +#### Response + +``` +0x123 +``` + +--- + ## Decrypt DDO ### `HTTP` POST /api/services/decrypt diff --git a/docs/PolicyServer.md b/docs/PolicyServer.md index e092a7532..20c4ea956 100644 --- a/docs/PolicyServer.md +++ b/docs/PolicyServer.md @@ -95,6 +95,18 @@ Called whenever a new encrypt command is received by Ocean Node } ``` +### encryptFile + +Called whenever a new encryptFile command is received by Ocean Node + +```json +{ + "action": "encrypt", + "policyServer": {}, + "file"?: object +} +``` + ### decrypt Called whenever a new decrypt command is received by Ocean Node diff --git a/src/@types/commands.ts b/src/@types/commands.ts index 4ebb78db4..4fbe633cc 100644 --- a/src/@types/commands.ts +++ b/src/@types/commands.ts @@ -120,16 +120,23 @@ export interface DecryptDDOCommand extends Command { } export interface EncryptCommand extends Command { + nonce: string + consumerAddress: string + signature: string blob: string encoding?: string encryptionType?: EncryptMethod.AES | EncryptMethod.ECIES + policyServer?: any // object to pass to policy server } export interface EncryptFileCommand extends Command { + nonce: string + consumerAddress: string + signature: string encryptionType?: EncryptMethod.AES | EncryptMethod.ECIES files?: BaseFileObject rawData?: Buffer - // UrlFileObject | ArweaveFileObject | IpfsFileObject + policyServer?: any // object to pass to policy server } export interface NonceCommand extends Command { diff --git a/src/components/core/handler/encryptHandler.ts b/src/components/core/handler/encryptHandler.ts index 81af4c4a2..f2acf4d0b 100644 --- a/src/components/core/handler/encryptHandler.ts +++ b/src/components/core/handler/encryptHandler.ts @@ -4,7 +4,8 @@ import { EncryptCommand, EncryptFileCommand } from '../../../@types/commands.js' import * as base58 from 'base58-js' import { Readable } from 'stream' import { Storage } from '../../storage/index.js' -import { getConfiguration } from '../../../utils/index.js' +import { getConfiguration, isPolicyServerConfigured } from '../../../utils/index.js' +import { PolicyServer } from '../../policyServer/index.js' import { EncryptMethod } from '../../../@types/fileObject.js' import { ValidateParams, @@ -49,9 +50,41 @@ export class EncryptHandler extends CommandHandler { async handle(task: EncryptCommand): Promise { const validationResponse = await this.verifyParamsAndRateLimits(task) + if (this.shouldDenyTaskHandling(validationResponse)) { return validationResponse } + const isAuthRequestValid = await this.validateTokenOrSignature( + task.authorization, + task.consumerAddress, + task.nonce, + task.signature, + String(task.nonce) + ) + if (isAuthRequestValid.status.httpStatus !== 200) { + return isAuthRequestValid + } + + if (isPolicyServerConfigured()) { + const policyServer = new PolicyServer() + const response = await policyServer.checkEncrypt( + task.consumerAddress, + task.policyServer + ) + if (!response) { + CORE_LOGGER.logMessage( + `Error: Encrypt for ${task.consumerAddress} was denied`, + true + ) + return { + stream: null, + status: { + httpStatus: 403, + error: `Error: Encrypt for ${task.consumerAddress} was denied` + } + } + } + } try { const oceanNode = this.getOceanNode() // prepare an empty array in case if @@ -112,6 +145,39 @@ export class EncryptFileHandler extends CommandHandler { if (this.shouldDenyTaskHandling(validationResponse)) { return validationResponse } + const isAuthRequestValid = await this.validateTokenOrSignature( + task.authorization, + task.consumerAddress, + task.nonce, + task.signature, + String(task.nonce) + ) + if (isAuthRequestValid.status.httpStatus !== 200) { + return isAuthRequestValid + } + + if (isPolicyServerConfigured()) { + const policyServer = new PolicyServer() + const response = await policyServer.checkEncryptFile( + task.consumerAddress, + task.policyServer, + task.files + ) + if (!response) { + CORE_LOGGER.logMessage( + `Error: EncryptFile for ${task.consumerAddress} was denied`, + true + ) + return { + stream: null, + status: { + httpStatus: 403, + error: `Error: EncryptFile for ${task.consumerAddress} was denied` + } + } + } + } + try { const oceanNode = this.getOceanNode() const config = await getConfiguration() diff --git a/src/components/core/handler/handler.ts b/src/components/core/handler/handler.ts index 096669fea..96f848cea 100644 --- a/src/components/core/handler/handler.ts +++ b/src/components/core/handler/handler.ts @@ -182,17 +182,19 @@ export abstract class CommandHandler ): Promise { const oceanNode = this.getOceanNode() const auth = oceanNode.getAuth() - const isAuthRequestValid = await auth.validateAuthenticationOrToken({ - token: authToken, - address, - nonce, - signature, - message - }) - if (!isAuthRequestValid.valid) { - return { - stream: null, - status: { httpStatus: 401, error: isAuthRequestValid.error } + if (auth) { + const isAuthRequestValid = await auth.validateAuthenticationOrToken({ + token: authToken, + address, + nonce, + signature, + message + }) + if (!isAuthRequestValid.valid) { + return { + stream: null, + status: { httpStatus: 401, error: isAuthRequestValid.error } + } } } diff --git a/src/components/httpRoutes/provider.ts b/src/components/httpRoutes/provider.ts index bc45661af..f44a05744 100644 --- a/src/components/httpRoutes/provider.ts +++ b/src/components/httpRoutes/provider.ts @@ -48,7 +48,10 @@ providerRoutes.post(`${SERVICES_API_BASE_PATH}/encrypt`, async (req, res) => { encoding: 'string', encryptionType: EncryptMethod.ECIES, command: PROTOCOL_COMMANDS.ENCRYPT, - caller: req.caller + caller: req.caller, + nonce: req.query.nonce as string, + consumerAddress: req.query.consumerAddress as string, + signature: req.query.signature as string }) if (result.stream) { const encryptedData = await streamToString(result.stream as Readable) @@ -100,7 +103,10 @@ providerRoutes.post(`${SERVICES_API_BASE_PATH}/encryptFile`, async (req, res) => rawData: input, encryptionType: encryptMethod, command: PROTOCOL_COMMANDS.ENCRYPT_FILE, - caller: req.caller + caller: req.caller, + nonce: req.query.nonce as string, + consumerAddress: req.query.consumerAddress as string, + signature: req.query.signature as string }) return result } @@ -116,7 +122,10 @@ providerRoutes.post(`${SERVICES_API_BASE_PATH}/encryptFile`, async (req, res) => files: req.body as BaseFileObject, encryptionType: encryptMethod, command: PROTOCOL_COMMANDS.ENCRYPT_FILE, - caller: req.caller + caller: req.caller, + nonce: req.query.nonce as string, + consumerAddress: req.query.consumerAddress as string, + signature: req.query.signature as string }) return await writeResponse(result, encryptMethod) // raw data on body diff --git a/src/components/policyServer/index.ts b/src/components/policyServer/index.ts index 05a538906..87deb51ea 100644 --- a/src/components/policyServer/index.ts +++ b/src/components/policyServer/index.ts @@ -1,6 +1,7 @@ import { DDO } from '@oceanprotocol/ddo-js' import { PolicyServerResult } from '../../@types/policyServer.js' import { isDefined } from '../../utils/util.js' +import { BaseFileObject } from '../../@types/fileObject.js' export class PolicyServer { serverUrl: string @@ -69,6 +70,32 @@ export class PolicyServer { return await this.askServer(command) } + async checkEncrypt( + consumerAddress: string, + policyServer: any + ): Promise { + const command = { + action: 'encrypt', + consumerAddress, + policyServer + } + return await this.askServer(command) + } + + async checkEncryptFile( + consumerAddress: string, + policyServer: any, + files?: BaseFileObject + ): Promise { + const command = { + action: 'encryptFile', + consumerAddress, + policyServer, + files + } + return await this.askServer(command) + } + async checkDownload( documentId: string, ddo: DDO, diff --git a/src/test/integration/encryptFile.test.ts b/src/test/integration/encryptFile.test.ts index 988cf978a..aa55e3ab5 100644 --- a/src/test/integration/encryptFile.test.ts +++ b/src/test/integration/encryptFile.test.ts @@ -7,6 +7,7 @@ import { Readable } from 'stream' import { EncryptFileHandler } from '../../components/core/handler/encryptHandler.js' import { EncryptFileCommand } from '../../@types/commands' import { EncryptMethod, FileObjectType, UrlFileObject } from '../../@types/fileObject.js' +import { Wallet, ethers } from 'ethers' import fs from 'fs' import { OverrideEnvConfig, @@ -21,6 +22,7 @@ describe('Encrypt File', () => { let config: OceanNodeConfig let oceanNode: OceanNode let previousConfiguration: OverrideEnvConfig[] + let anotherConsumerWallet: Wallet before(async () => { previousConfiguration = await setupEnvironment( @@ -33,11 +35,28 @@ describe('Encrypt File', () => { config = await getConfiguration(true) // Force reload the configuration const dbconn = await Database.init(config.dbConfig) oceanNode = await OceanNode.getInstance(config, dbconn) + anotherConsumerWallet = new ethers.Wallet( + '0xef4b441145c1d0f3b4bc6d61d29f5c6e502359481152f869247c7a4244d45209' + ) }) it('should encrypt files', async () => { + const wallet = new ethers.Wallet( + '0xef4b441145c1d0f3b4bc6d61d29f5c6e502359481152f869247c7a4244d45209' + ) + const nonce = Date.now().toString() + const message = String(nonce) + const consumerMessage = ethers.solidityPackedKeccak256( + ['bytes'], + [ethers.hexlify(ethers.toUtf8Bytes(message))] + ) + const messageHashBytes = ethers.toBeArray(consumerMessage) + const signature = await wallet.signMessage(messageHashBytes) const encryptFileTask: EncryptFileCommand = { command: PROTOCOL_COMMANDS.ENCRYPT_FILE, + nonce, + consumerAddress: await wallet.getAddress(), + signature, encryptionType: EncryptMethod.AES, files: { type: FileObjectType.URL, @@ -63,10 +82,21 @@ describe('Encrypt File', () => { it('should encrypt raw data file on body (AES)', async () => { // should return a buffer const file: Buffer = fs.readFileSync('src/test/data/organizations-100.aes') + const nonce = Date.now().toString() + const message = String(nonce) + const consumerMessage = ethers.solidityPackedKeccak256( + ['bytes'], + [ethers.hexlify(ethers.toUtf8Bytes(message))] + ) + const messageHashBytes = ethers.toBeArray(consumerMessage) + const signature = await anotherConsumerWallet.signMessage(messageHashBytes) const encryptFileTask: EncryptFileCommand = { command: PROTOCOL_COMMANDS.ENCRYPT_FILE, encryptionType: EncryptMethod.AES, - rawData: file + rawData: file, + nonce, + consumerAddress: await anotherConsumerWallet.getAddress(), + signature } const response = await new EncryptFileHandler(oceanNode).handle(encryptFileTask) @@ -85,10 +115,21 @@ describe('Encrypt File', () => { it('should encrypt raw data file on body (ECIES)', async () => { // should return a buffer const file: Buffer = fs.readFileSync('src/test/data/organizations-100.aes') + const nonce = Date.now().toString() + const message = String(nonce) + const consumerMessage = ethers.solidityPackedKeccak256( + ['bytes'], + [ethers.hexlify(ethers.toUtf8Bytes(message))] + ) + const messageHashBytes = ethers.toBeArray(consumerMessage) + const signature = await anotherConsumerWallet.signMessage(messageHashBytes) const encryptFileTask: EncryptFileCommand = { command: PROTOCOL_COMMANDS.ENCRYPT_FILE, encryptionType: EncryptMethod.ECIES, - rawData: file + rawData: file, + nonce, + consumerAddress: await anotherConsumerWallet.getAddress(), + signature } const response = await new EncryptFileHandler(oceanNode).handle(encryptFileTask) @@ -105,6 +146,14 @@ describe('Encrypt File', () => { }) it('should return unknown file type', async () => { + const nonce = Date.now().toString() + const message = String(nonce) + const consumerMessage = ethers.solidityPackedKeccak256( + ['bytes'], + [ethers.hexlify(ethers.toUtf8Bytes(message))] + ) + const messageHashBytes = ethers.toBeArray(consumerMessage) + const signature = await anotherConsumerWallet.signMessage(messageHashBytes) const encryptFileTask: EncryptFileCommand = { command: PROTOCOL_COMMANDS.ENCRYPT_FILE, encryptionType: EncryptMethod.AES, @@ -112,7 +161,10 @@ describe('Encrypt File', () => { type: 'Unknown', url: 'Unknown', method: 'Unknown' - } as UrlFileObject + } as UrlFileObject, + nonce, + consumerAddress: await anotherConsumerWallet.getAddress(), + signature } const response = await new EncryptFileHandler(oceanNode).handle(encryptFileTask) diff --git a/src/test/unit/commands.test.ts b/src/test/unit/commands.test.ts index ec4385fd6..43656170a 100644 --- a/src/test/unit/commands.test.ts +++ b/src/test/unit/commands.test.ts @@ -52,14 +52,18 @@ import { ReindexTxHandler } from '../../components/core/admin/reindexTxHandler.j import { ReindexChainHandler } from '../../components/core/admin/reindexChainHandler.js' import { CollectFeesHandler } from '../../components/core/admin/collectFeesHandler.js' import { GetJobsHandler } from '../../components/core/handler/getJobs.js' - +import { Wallet, ethers } from 'ethers' describe('Commands and handlers', () => { let node: OceanNode + let consumerAccount: Wallet + let consumerAddress: string before(async () => { const config = await getConfiguration() const keyManager = new KeyManager(config) node = OceanNode.getInstance(config, null, null, null, null, keyManager, null, true) + consumerAccount = new Wallet(process.env.PRIVATE_KEY) + consumerAddress = await consumerAccount.getAddress() }) it('Check that all supported commands have registered handlers', () => { @@ -78,7 +82,7 @@ describe('Commands and handlers', () => { expect(SUPPORTED_PROTOCOL_COMMANDS.length).to.be.equal(handlers.length) }) - it('Check that all commands are validating required parameters', () => { + it('Check that all commands are validating required parameters', async () => { // To make sure we do not forget to register anything on supported commands // downloadHandler @@ -132,9 +136,20 @@ describe('Commands and handlers', () => { const encryptHandler: EncryptHandler = CoreHandlersRegistry.getInstance( node ).getHandler(PROTOCOL_COMMANDS.ENCRYPT) + let nonce = Date.now().toString() + let message = String(nonce) + let consumerMessage = ethers.solidityPackedKeccak256( + ['bytes'], + [ethers.hexlify(ethers.toUtf8Bytes(message))] + ) + let messageHashBytes = ethers.toBeArray(consumerMessage) + let signature = await consumerAccount.signMessage(messageHashBytes) const encryptCommand: EncryptCommand = { blob: '1425252525', - command: PROTOCOL_COMMANDS.ENCRYPT + command: PROTOCOL_COMMANDS.ENCRYPT, + nonce, + consumerAddress, + signature } expect(encryptHandler.validate(encryptCommand).valid).to.be.equal(true) delete encryptCommand.blob @@ -144,9 +159,20 @@ describe('Commands and handlers', () => { const encryptFileHandler: EncryptFileHandler = CoreHandlersRegistry.getInstance( node ).getHandler(PROTOCOL_COMMANDS.ENCRYPT_FILE) + nonce = (parseFloat(nonce) + 1).toString() + message = String(nonce) + consumerMessage = ethers.solidityPackedKeccak256( + ['bytes'], + [ethers.hexlify(ethers.toUtf8Bytes(message))] + ) + messageHashBytes = ethers.toBeArray(consumerMessage) + signature = await consumerAccount.signMessage(messageHashBytes) const encryptFileCommand: EncryptFileCommand = { rawData: Buffer.from('12345'), - command: PROTOCOL_COMMANDS.ENCRYPT_FILE + command: PROTOCOL_COMMANDS.ENCRYPT_FILE, + nonce, + consumerAddress, + signature } expect(encryptFileHandler.validate(encryptFileCommand).valid).to.be.equal(true) delete encryptFileCommand.rawData diff --git a/src/test/unit/download.test.ts b/src/test/unit/download.test.ts index 9c2014039..da45d1d8d 100644 --- a/src/test/unit/download.test.ts +++ b/src/test/unit/download.test.ts @@ -19,6 +19,7 @@ import { validateFilesStructure } from '../../components/core/handler/downloadHa import { AssetUtils, isConfidentialChainDDO } from '../../utils/asset.js' import { DEVELOPMENT_CHAIN_ID, KNOWN_CONFIDENTIAL_EVMS } from '../../utils/address.js' import { DDO } from '@oceanprotocol/ddo-js' +import { Wallet, ethers } from 'ethers' let envOverrides: OverrideEnvConfig[] let config: OceanNodeConfig @@ -26,6 +27,7 @@ let db: Database let oceanNode: OceanNode describe('Should validate files structure for download', () => { + let consumerAccount: Wallet before(async () => { envOverrides = buildEnvOverrideConfig( [ENVIRONMENT_VARIABLES.PRIVATE_KEY], @@ -35,6 +37,7 @@ describe('Should validate files structure for download', () => { config = await getConfiguration(true) db = await Database.init(config.dbConfig) oceanNode = OceanNode.getInstance(config, db) + consumerAccount = new Wallet(process.env.PRIVATE_KEY) }) const ddoObj: DDO = { @@ -81,11 +84,22 @@ describe('Should validate files structure for download', () => { // encrypts the assetURL using the encrypt handler (what goes into services files at publish time), // and decrypts the data back to the original format const getDecryptedData = async function () { + const nonce = Date.now().toString() + const message = String(nonce) + const consumerMessage = ethers.solidityPackedKeccak256( + ['bytes'], + [ethers.hexlify(ethers.toUtf8Bytes(message))] + ) + const messageHashBytes = ethers.toBeArray(consumerMessage) + const signature = await consumerAccount.signMessage(messageHashBytes) const result = await new EncryptHandler(oceanNode).handle({ blob: JSON.stringify(assetURL), encoding: 'string', encryptionType: EncryptMethod.ECIES, - command: PROTOCOL_COMMANDS.ENCRYPT + command: PROTOCOL_COMMANDS.ENCRYPT, + nonce, + consumerAddress: await consumerAccount.getAddress(), + signature }) const encryptedData: string = await streamToString(result.stream as Readable) @@ -134,11 +148,22 @@ describe('Should validate files structure for download', () => { const newAssetURL = structuredClone(assetURL) newAssetURL.nftAddress = otherNFTAddress newAssetURL.datatokenAddress = otherDatatokenAddress + const nonce = Date.now().toString() + const message = String(nonce) + const consumerMessage = ethers.solidityPackedKeccak256( + ['bytes'], + [ethers.hexlify(ethers.toUtf8Bytes(message))] + ) + const messageHashBytes = ethers.toBeArray(consumerMessage) + const signature = await consumerAccount.signMessage(messageHashBytes) const result = await new EncryptHandler(oceanNode).handle({ blob: JSON.stringify(newAssetURL), encoding: 'string', encryptionType: EncryptMethod.ECIES, - command: PROTOCOL_COMMANDS.ENCRYPT + command: PROTOCOL_COMMANDS.ENCRYPT, + nonce, + consumerAddress: await consumerAccount.getAddress(), + signature }) const encryptedFilesData: string = await streamToString(result.stream as Readable)