-
Notifications
You must be signed in to change notification settings - Fork 10
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
6 changed files
with
280 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
import keystoreArgument from './keystore'; | ||
import keystorePathArgument from './keystore-path'; | ||
import ownerNonceArgument from './owner-nonce'; | ||
import operatorIdsArgument from './operator-ids'; | ||
import ownerAddressArgument from './owner-address'; | ||
import keystorePasswordArgument from './password'; | ||
import outputFolderArgument from './output-folder'; | ||
import operatorPublicKeysArgument from './operator-public-keys'; | ||
|
||
export { | ||
keystoreArgument, | ||
keystorePathArgument, | ||
ownerNonceArgument, | ||
operatorIdsArgument, | ||
ownerAddressArgument, | ||
keystorePasswordArgument, | ||
outputFolderArgument, | ||
operatorPublicKeysArgument | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
import { fileExistsValidator, jsonFileValidator, sanitizePath } from '../validators'; | ||
|
||
/** | ||
* Keystore argument validates if keystore file exists and is valid keystore file. | ||
*/ | ||
export default { | ||
arg1: '-ks', | ||
arg2: '--keystore', | ||
options: { | ||
required: false, | ||
type: String, | ||
help: 'The validator keystore file path. Only one keystore file can be specified using this argument' | ||
}, | ||
interactive: { | ||
options: { | ||
type: 'text', | ||
message: 'Provide the keystore file path', | ||
validateSingle: (filePath: string): any => { | ||
filePath = sanitizePath(String(filePath).trim()); | ||
let isValid = fileExistsValidator(filePath); | ||
if (isValid !== true) { | ||
return isValid; | ||
} | ||
isValid = jsonFileValidator(filePath); | ||
if (isValid !== true) { | ||
return isValid; | ||
} | ||
return true; | ||
}, | ||
} | ||
} | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
import { sanitizePath, fileExistsValidator, jsonFileValidator } from './file'; | ||
import { keystorePasswordValidator } from './keystore-password'; | ||
import { isOperatorsLengthValid } from "./operator-ids"; | ||
import { operatorPublicKeyValidator } from './operator'; | ||
|
||
export { | ||
sanitizePath, | ||
jsonFileValidator, | ||
fileExistsValidator, | ||
isOperatorsLengthValid, | ||
keystorePasswordValidator, | ||
operatorPublicKeyValidator, | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
import { OpeatorsListValidator } from './operator-unique'; | ||
import { PublicKeyValidator } from './public-key'; | ||
import { OwnerAddressValidator } from './owner-address'; | ||
import { OwnerNonceValidator } from './owner-nonce'; | ||
import { MatchLengthValidator } from './match'; | ||
import { OpeatorPublicKeyValidator } from './operator-public-key'; | ||
|
||
export { | ||
OpeatorsListValidator, | ||
PublicKeyValidator, | ||
OwnerAddressValidator, | ||
OwnerNonceValidator, | ||
MatchLengthValidator, | ||
OpeatorPublicKeyValidator, | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,193 @@ | ||
import * as ethers from 'ethers'; | ||
import * as web3Helper from '../helpers/web3.helper'; | ||
import { | ||
IsOptional, | ||
ValidateNested, | ||
validateSync | ||
} from 'class-validator'; | ||
|
||
import { KeySharesData } from './KeySharesData/KeySharesData'; | ||
import { KeySharesPayload } from './KeySharesData/KeySharesPayload'; | ||
import { EncryptShare } from '../Encryption/Encryption'; | ||
import { IKeySharesPartitialData } from './KeySharesData/IKeySharesData'; | ||
import { IOperator } from './KeySharesData/IOperator'; | ||
import { operatorSortedList } from '../helpers/operator.helper'; | ||
import { OwnerAddressFormatError, OwnerNonceFormatError } from '../exceptions/keystore'; | ||
|
||
export interface IKeySharesPayloadData { | ||
publicKey: string, | ||
operators: IOperator[], | ||
encryptedShares: EncryptShare[], | ||
} | ||
|
||
export interface IKeySharesToSignatureData { | ||
ownerAddress: string, | ||
ownerNonce: number, | ||
privateKey: string, | ||
} | ||
|
||
export interface IKeySharesFromSignatureData { | ||
ownerAddress: string, | ||
ownerNonce: number, | ||
publicKey: string, | ||
} | ||
|
||
const SIGNATURE_LENGHT = 192; | ||
const PUBLIC_KEY_LENGHT = 96; | ||
|
||
/** | ||
* Key shares file data interface. | ||
*/ | ||
export class KeySharesItem { | ||
@IsOptional() | ||
@ValidateNested() | ||
public data: KeySharesData; | ||
|
||
@IsOptional() | ||
@ValidateNested() | ||
public payload: KeySharesPayload; | ||
|
||
constructor() { | ||
this.data = new KeySharesData(); | ||
this.payload = new KeySharesPayload(); | ||
} | ||
|
||
/** | ||
* Build payload from operators list, encrypted shares and validator public key | ||
* @param publicKey | ||
* @param operatorIds | ||
* @param encryptedShares | ||
*/ | ||
async buildPayload(metaData: IKeySharesPayloadData, toSignatureData: IKeySharesToSignatureData): Promise<any> { | ||
const { | ||
ownerAddress, | ||
ownerNonce, | ||
privateKey, | ||
} = toSignatureData; | ||
|
||
if (!Number.isInteger(ownerNonce) || ownerNonce < 0) { | ||
throw new OwnerNonceFormatError(ownerNonce, 'Owner nonce is not positive integer'); | ||
} | ||
|
||
let address; | ||
try { | ||
address = web3Helper.web3.utils.toChecksumAddress(ownerAddress); | ||
} catch { | ||
throw new OwnerAddressFormatError(ownerAddress, 'Owner address is not a valid Ethereum address'); | ||
} | ||
|
||
const payload = this.payload.build({ | ||
publicKey: metaData.publicKey, | ||
operatorIds: operatorSortedList(metaData.operators).map(operator => operator.id), | ||
encryptedShares: metaData.encryptedShares, | ||
}); | ||
|
||
const signature = await web3Helper.buildSignature(`${address}:${ownerNonce}`, privateKey); | ||
const signSharesBytes = web3Helper.hexArrayToBytes([signature, payload.sharesData]); | ||
|
||
payload.sharesData = `0x${signSharesBytes.toString('hex')}`; | ||
|
||
// verify signature | ||
await this.validateSingleShares(payload.sharesData, { | ||
ownerAddress, | ||
ownerNonce, | ||
publicKey: await web3Helper.privateToPublicKey(privateKey), | ||
}); | ||
|
||
return payload; | ||
} | ||
|
||
|
||
async validateSingleShares(shares: string, fromSignatureData: IKeySharesFromSignatureData): Promise<void> { | ||
const { ownerAddress, ownerNonce, publicKey } = fromSignatureData; | ||
|
||
if (!Number.isInteger(ownerNonce) || ownerNonce < 0) { | ||
throw new OwnerNonceFormatError(ownerNonce, 'Owner nonce is not positive integer'); | ||
} | ||
|
||
const address = web3Helper.web3.utils.toChecksumAddress(ownerAddress); | ||
const signaturePt = shares.replace('0x', '').substring(0, SIGNATURE_LENGHT); | ||
|
||
await web3Helper.validateSignature(`${address}:${ownerNonce}`, `0x${signaturePt}`, publicKey); | ||
} | ||
|
||
/** | ||
* Build shares from bytes string and operators list length | ||
* @param bytes | ||
* @param operatorCount | ||
*/ | ||
buildSharesFromBytes(bytes: string, operatorCount: number): any { | ||
const sharesPt = bytes.replace('0x', '').substring(SIGNATURE_LENGHT); | ||
|
||
const pkSplit = sharesPt.substring(0, operatorCount * PUBLIC_KEY_LENGHT); | ||
const pkArray = ethers.utils.arrayify('0x' + pkSplit); | ||
const sharesPublicKeys = this.splitArray(operatorCount, pkArray) | ||
.map(item => ethers.utils.hexlify(item)); | ||
|
||
const eSplit = bytes.substring(operatorCount * PUBLIC_KEY_LENGHT); | ||
const eArray = ethers.utils.arrayify('0x' + eSplit); | ||
const encryptedKeys = this.splitArray(operatorCount, eArray).map(item => | ||
Buffer.from(ethers.utils.hexlify(item).replace('0x', ''), 'hex').toString( | ||
'base64', | ||
), | ||
); | ||
|
||
return { sharesPublicKeys, encryptedKeys }; | ||
} | ||
|
||
/** | ||
* Updates the current instance with partial data and payload, and validates. | ||
* @param data Partial key shares data. | ||
* @param payload Partial key shares payload. | ||
*/ | ||
update(data: IKeySharesPartitialData): void { | ||
this.data.update(data); | ||
this.validate(); | ||
} | ||
|
||
/** | ||
* Validate everything | ||
*/ | ||
validate(): any { | ||
validateSync(this); | ||
} | ||
|
||
/** | ||
* Initialise from JSON or object data. | ||
*/ | ||
async fromJson(content: string | any): Promise<KeySharesItem> { | ||
const body = typeof content === 'string' ? JSON.parse(content) : content; | ||
this.data.update(body.data); | ||
this.payload.update(body.payload); | ||
this.validate(); | ||
// Custom validation: verify signature | ||
await this.validateSingleShares(this.payload.sharesData, { | ||
ownerAddress: this.data.ownerAddress as string, | ||
ownerNonce: this.data.ownerNonce as number, | ||
publicKey: this.data.publicKey as string, | ||
}); | ||
|
||
return this; | ||
} | ||
|
||
/** | ||
* Stringify key shares to be ready for saving in file. | ||
*/ | ||
toJson(): string { | ||
return JSON.stringify({ | ||
data: this.data || null, | ||
payload: this.payload || null, | ||
}, null, 2); | ||
} | ||
|
||
private splitArray(parts: number, arr: Uint8Array) { | ||
const partLength = Math.floor(arr.length / parts); | ||
const partsArr = []; | ||
for (let i = 0; i < parts; i++) { | ||
const start = i * partLength; | ||
const end = start + partLength; | ||
partsArr.push(arr.slice(start, end)); | ||
} | ||
return partsArr; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
export class BaseCustomError extends Error { | ||
constructor(message: string) { | ||
super(message); | ||
this.name = this.constructor.name; | ||
Error.captureStackTrace(this, this.constructor); | ||
this.stack = `${this.name}: ${this.message}`; // Customizing stack | ||
} | ||
} |