import { Err , Ok , parseJsonAsResult , Result , trimLeading0x } from '@celo/base' ; import { Address , publicKeyToAddress } from '@celo/utils/lib/address' ; import { ensureCompressed , ensureUncompressed , trimUncompressedPrefix } from '@celo/utils/lib/ecdh' ; import { AES128Decrypt , AES128Encrypt , Encrypt , IV_LENGTH } from '@celo/utils/lib/ecies' ; import { EIP712Object , EIP712TypedData } from '@celo/utils/lib/sign-typed-data-utils' ; import { createHmac , randomBytes } from 'crypto' ; import { keccak256 } from 'ethereumjs-util' ; import { isLeft } from 'fp-ts/lib/Either' ; import * as t from 'io-ts' ; import { join , sep } from 'path' ; import { OffchainDataWrapper , OffchainErrorTypes } from '../offchain-data-wrapper' ; import { InvalidDataError , InvalidKey , OffchainError , SchemaErrors , SchemaErrorTypes , UnavailableKey , } from './accessors/errors' ; const KEY_LENGTH = 16 ; function getCiphertextLabel ( path , sharedSecret , senderPublicKey , receiverPublicKey ) { const senderPublicKeyBuffer = Buffer . from ( ensureCompressed ( senderPublicKey ) , 'hex' ) ; const receiverPublicKeyBuffer = Buffer . from ( ensureCompressed ( receiverPublicKey ) , 'hex' ) ; const label = createHmac ( 'sha256' , sharedSecret ) . update ( Buffer . concat ( [ senderPublicKeyBuffer , receiverPublicKeyBuffer , Buffer . from ( path ) ] ) ) . digest ( 'hex' ) ; return join ( sep , 'ciphertexts' , label ) ; } param wrapper param dataPath param key param toAddress const distributeSymmetricKey = async ( wrapper , dataPath , key , toAddress ) : Promise < void | SchemaErrors > => { const accounts = await wrapper . kit . contracts . getAccounts ( ) ; const [ fromPubKey , toPubKey ] = await Promise . all ( [ accounts . getDataEncryptionKey ( wrapper . self ) , accounts . getDataEncryptionKey ( toAddress ) , ] ) ; if ( fromPubKey === null ) { return new UnavailableKey ( wrapper . self ) ; } if ( toPubKey === null ) { return new UnavailableKey ( toAddress ) ; } const wallet = wrapper . kit . getWallet ( ) ! ; const sharedSecret = await wallet . computeSharedSecret ( publicKeyToAddress ( fromPubKey ) , toPubKey ) ; const computedDataPath = getCiphertextLabel ( `${ dataPath }.key` , sharedSecret , fromPubKey , toPubKey ) ; const encryptedData = Encrypt ( Buffer . from ( trimUncompressedPrefix ( ensureUncompressed ( toPubKey ) ) , 'hex' ) , key ) ; const signature = await signBuffer ( wrapper , computedDataPath , encryptedData ) ; const writeError = await wrapper . writeDataTo ( encryptedData , Buffer . from ( trimLeading0x ( signature ) , 'hex' ) , computedDataPath ) ; if ( writeError ) { return new OffchainError ( writeError ) ; } } ; param wrapper param dataPath param symmetricKey async function fetchOrGenerateKey ( wrapper , dataPath , symmetricKey ? ) { if ( symmetricKey ) { return Ok ( symmetricKey ) ; } const existingKey = await readSymmetricKey ( wrapper , dataPath , wrapper . self ) ; if ( existingKey . ok ) { return Ok ( existingKey . result ) ; } if ( existingKey . error . errorType === SchemaErrorTypes . OffchainError && ( existingKey . error . error . errorType === OffchainErrorTypes . NoStorageRootProvidedData || existingKey . error . error . errorType === OffchainErrorTypes . FetchError ) ) { return Ok ( randomBytes ( 16 ) ) ; } return Err ( existingKey . error ) ; } param wrapper param dataPath param data param toAddresses param symmetricKey export const writeEncrypted = async ( wrapper , dataPath , data , toAddresses , symmetricKey ? ) : Promise < SchemaErrors | void > => { const fetchKey = await fetchOrGenerateKey ( wrapper , dataPath , symmetricKey ) ; if ( ! fetchKey . ok ) { return fetchKey . error ; } const iv = randomBytes ( 16 ) ; const payload = AES128Encrypt ( fetchKey . result , iv , data ) ; const signature = await signBuffer ( wrapper , `${ dataPath }.enc` , payload ) ; const writeError = await wrapper . writeDataTo ( payload , Buffer . from ( trimLeading0x ( signature ) , 'hex' ) , `${ dataPath }.enc` ) ; if ( writeError ) { return new OffchainError ( writeError ) ; } const firstWriteError = ( await Promise . all ( [ wrapper . self , ... toAddresses ] . map ( async ( toAddress ) => distributeSymmetricKey ( wrapper , dataPath , fetchKey . result , toAddress ) ) ) ) . find ( Boolean ) ; return firstWriteError ; } ; export const writeSymmetricKeys = async ( wrapper , dataPath , toAddresses , symmetricKey ? ) : Promise < SchemaErrors | void > => { const fetchKey = await fetchOrGenerateKey ( wrapper , dataPath , symmetricKey ) ; if ( ! fetchKey . ok ) { return fetchKey . error ; } const firstWriteError = ( await Promise . all ( toAddresses . map ( async ( toAddress ) => distributeSymmetricKey ( wrapper , dataPath , fetchKey . result , toAddress ) ) ) ) . find ( Boolean ) ; return firstWriteError ; } ; param wrapper param dataPath param senderAddress const readSymmetricKey = async ( wrapper , dataPath , senderAddress ) : Promise < Result < Buffer , SchemaErrors > > => { const accounts = await wrapper . kit . contracts . getAccounts ( ) ; const wallet = wrapper . kit . getWallet ( ) ! ; const [ readerPubKey , senderPubKey ] = await Promise . all ( [ accounts . getDataEncryptionKey ( wrapper . self ) , accounts . getDataEncryptionKey ( senderAddress ) , ] ) ; if ( readerPubKey === null ) { return Err ( new UnavailableKey ( wrapper . self ) ) ; } if ( senderPubKey === null ) { return Err ( new UnavailableKey ( senderAddress ) ) ; } const readerPublicKeyAddress = publicKeyToAddress ( readerPubKey ) ; if ( ! wallet . hasAccount ( readerPublicKeyAddress ) ) { return Err ( new UnavailableKey ( readerPublicKeyAddress ) ) ; } const sharedSecret = await wallet . computeSharedSecret ( readerPublicKeyAddress , senderPubKey ) ; const computedDataPath = getCiphertextLabel ( `${ dataPath }.key` , sharedSecret , senderPubKey , readerPubKey ) ; const encryptedPayload = await wrapper . readDataFromAsResult ( senderAddress , computedDataPath , true ) ; if ( ! encryptedPayload . ok ) { return Err ( new OffchainError ( encryptedPayload . error ) ) ; } const payload = await wallet . decrypt ( readerPublicKeyAddress , encryptedPayload . result ) ; return Ok ( payload ) ; } ; param wrapper param dataPath param senderAddress export const readEncrypted = async ( wrapper , dataPath , senderAddress ) : Promise < Result < Buffer , SchemaErrors > > => { const encryptedPayloadPath = `${ dataPath }.enc` ; const [ payload , key ] = await Promise . all ( [ wrapper . readDataFromAsResult ( senderAddress , encryptedPayloadPath , true ) , readSymmetricKey ( wrapper , dataPath , senderAddress ) , ] ) ; if ( ! payload . ok ) { return Err ( new OffchainError ( payload . error ) ) ; } if ( ! key . ok ) { return Err ( key . error ) ; } if ( key . result . length !== KEY_LENGTH ) { return Err ( new InvalidKey ( ) ) ; } if ( payload . result . length < IV_LENGTH ) { return Err ( new InvalidDataError ( ) ) ; } return Ok ( AES128Decrypt ( key . result , payload . result . slice ( 0 , IV_LENGTH ) , payload . result . slice ( IV_LENGTH ) ) ) ; } ; export const deserialize = < DataType > ( type , buf ) : Result < DataType , SchemaErrors > => { const dataAsJson = parseJsonAsResult ( buf . toString ( ) ) ; if ( ! dataAsJson . ok ) { return Err ( new InvalidDataError ( ) ) ; } const parsedDataAsType = type . decode ( dataAsJson . result ) ; if ( isLeft ( parsedDataAsType ) ) { return Err ( new InvalidDataError ( ) ) ; } return Ok ( parsedDataAsType . right ) ; } ; export const buildEIP712TypedData = async < DataType > ( wrapper , path , data , type ? ) : Promise < EIP712TypedData > => { const chainId = await wrapper . kit . connection . chainId ( ) ; const EIP712Domain = [ { name : 'name' , type : 'string' } , { name : 'version' , type : 'string' } , { name : 'chainId' , type : 'uint256' } , ] ; let types = { } ; let message = { } ; if ( Buffer . isBuffer ( data ) ) { types = { ClaimWithPath : [ { name : 'path' , type : 'string' } , { name : 'hash' , type : 'string' } , ] } ; message = { hash : keccak256 ( data ) . toString ( 'hex' ) } ; } else { const Claim = buildEIP712Schema ( type ! ) ; types = { Claim , ClaimWithPath : [ { name : 'path' , type : 'string' } , { name : 'payload' , type : 'Claim' } , ] } ; message = { payload : ( data as unknown ) as EIP712Object } ; } return { types : { EIP712Domain , ... types } , domain : { name : 'CIP8 Claim' , version : '1.0.0' , chainId } , primaryType : 'ClaimWithPath' , message : { path , ... message } } ; } ; export const signBuffer = async ( wrapper , dataPath , buf ) => { const typedData = await buildEIP712TypedData ( wrapper , dataPath , buf ) ; return wrapper . kit . getWallet ( ) ! . signTypedData ( wrapper . signer , typedData ) ; } ; const ioTsToSolidityTypeMapping = { [ t . string . _tag ] : 'string' , [ t . number . _tag ] : 'uint256' , [ t . boolean . _tag ] : 'bool' } ; type EIP712Schema = Array < { name : string ; type : string ; } > ; const buildEIP712Schema = < DataType > ( type ) : EIP712Schema => { const shape = type . props ; return Object . entries ( shape ) . reduce ( ( accum , [ key , value ] ) => { return [ ... accum , { name : key , type : ioTsToSolidityTypeMapping [ value . _tag ] || 'string' } , ] ; } , [ ] ) ; } ; function ensureTrailingSeparator ( str ) { if ( str [ str . length - 1 ] !== '/' ) { return `${ str }/` ; } return str ; } function trimLeadingSeparator ( str ) { if ( str [ 0 ] === '/' ) { return str . slice ( 1 ) ; } return str ; } param base param path export function resolvePath ( base , path ) { return new URL ( trimLeadingSeparator ( path ) , ensureTrailingSeparator ( base ) ) . href ; }