diff --git a/packages/keyring/src/keyring.ts b/packages/keyring/src/keyring.ts index 493904f5d2..c31338cfa0 100644 --- a/packages/keyring/src/keyring.ts +++ b/packages/keyring/src/keyring.ts @@ -5,7 +5,7 @@ import type { EncryptedJsonEncoding, Keypair, KeypairType } from '@polkadot/util import type { KeyringInstance, KeyringOptions, KeyringPair, KeyringPair$Json, KeyringPair$Meta } from './types'; import { assert, hexToU8a, isHex, isUndefined, stringToU8a } from '@polkadot/util'; -import { base64Decode, decodeAddress, ed25519PairFromSeed as ed25519FromSeed, encodeAddress, ethereumEncode, hdEthereum, keyExtractSuri, keyFromPath, mnemonicToLegacySeed, mnemonicToMiniSecret, secp256k1PairFromSeed as secp256k1FromSeed, sr25519PairFromSeed as sr25519FromSeed } from '@polkadot/util-crypto'; +import { base64Decode, decodeAddress, ed25519PairFromSeed as ed25519FromSeed, encodeAddress, encrypt as cryptoEncrypt, ethereumEncode, hdEthereum, keyExtractSuri, keyFromPath, mnemonicToLegacySeed, mnemonicToMiniSecret, secp256k1PairFromSeed as secp256k1FromSeed, sr25519PairFromSeed as sr25519FromSeed } from '@polkadot/util-crypto'; import { DEV_PHRASE } from './defaults'; import { createPair } from './pair'; @@ -296,4 +296,14 @@ export class Keyring implements KeyringInstance { public toJson (address: string | Uint8Array, passphrase?: string): KeyringPair$Json { return this.#pairs.get(address).toJson(passphrase); } + + /** + * @name encrypt + * @summary Encrypt a message using the given publickey + * @description Returns the encrypted message of the given message using the public key. + * The encrypted message can be decrypted by the corresponding keypair using keypair.decrypt() method + */ + public encrypt (message: string | Uint8Array, recipientPublicKey: string | Uint8Array, recipientKeyType?: KeypairType): Uint8Array { + return cryptoEncrypt(message, recipientPublicKey, recipientKeyType || this.type); + } } diff --git a/packages/keyring/src/pair/index.spec.ts b/packages/keyring/src/pair/index.spec.ts index 1776ed1254..a0f2bb6828 100644 --- a/packages/keyring/src/pair/index.spec.ts +++ b/packages/keyring/src/pair/index.spec.ts @@ -2,10 +2,11 @@ // SPDX-License-Identifier: Apache-2.0 import { hexToU8a, u8aToHex } from '@polkadot/util'; -import { cryptoWaitReady, encodeAddress as toSS58, setSS58Format } from '@polkadot/util-crypto'; +import { cryptoWaitReady, encodeAddress as toSS58, mnemonicGenerate, setSS58Format } from '@polkadot/util-crypto'; import { PAIRSSR25519 } from '../testing'; import { createTestPairs } from '../testingPairs'; +import { Keyring } from '..'; import { createPair } from '.'; const keyring = createTestPairs({ type: 'ed25519' }, false); @@ -234,6 +235,51 @@ describe('pair', (): void => { ).toThrow('Cannot sign with a locked key pair'); }); + it('allows encrypt/decrypt with ed25519 keypair', (): void => { + const message = new Uint8Array([0x61, 0x62, 0x63, 0x64, 0x65]); + const encryptedMessage = keyring.alice.encrypt(message, keyring.bob.publicKey); + + expect(keyring.bob.decrypt(encryptedMessage)).toEqual(message); + expect(keyring.alice.decrypt(encryptedMessage)).toEqual(null); + }); + + it('allows encrypt/decrypt with sr25519 keypair', (): void => { + const aliceSR25519KeyPair = new Keyring().createFromUri(mnemonicGenerate(), { name: 'sr25519 pair' }, 'sr25519'); + const bobSR25519KeyPair = new Keyring().createFromUri(mnemonicGenerate(), { name: 'sr25519 pair' }, 'sr25519'); + const message = new Uint8Array([0x61, 0x62, 0x63, 0x64, 0x65]); + const encryptedMessage = aliceSR25519KeyPair.encrypt(message, bobSR25519KeyPair.publicKey); + + expect(bobSR25519KeyPair.decrypt(encryptedMessage)).toEqual(message); + expect((): Uint8Array | null => aliceSR25519KeyPair.decrypt(encryptedMessage)).toThrow("Mac values don't match"); + }); + + it('allows encrypt for an ed25519 keypair', (): void => { + const message = new Uint8Array([0x61, 0x62, 0x63, 0x64, 0x65]); + const encryptedMessage = new Keyring().encrypt(message, keyring.alice.publicKey, keyring.alice.type); + + expect(keyring.alice.decrypt(encryptedMessage)).toEqual(message); + }); + + it('allows encrypt for an sr25519 keypair', (): void => { + const keyring = new Keyring(); + const sr25519KeyPair = keyring.createFromUri(mnemonicGenerate(), { name: 'sr25519 pair' }, 'sr25519'); + const message = new Uint8Array([0x61, 0x62, 0x63, 0x64, 0x65]); + const encryptedMessage = keyring.encrypt(message, sr25519KeyPair.publicKey, sr25519KeyPair.type); + + expect(sr25519KeyPair.decrypt(encryptedMessage)).toEqual(message); + }); + + it('fails to encrypt when locked', (): void => { + const aliceSR25519KeyPair = createPair({ toSS58, type: 'sr25519' }, { publicKey: keyring.alice.publicKey }); + const bobSR25519KeyPair = createPair({ toSS58, type: 'sr25519' }, { publicKey: keyring.alice.publicKey }); + const message = new Uint8Array([0x61, 0x62, 0x63, 0x64, 0x65]); + + expect(aliceSR25519KeyPair.isLocked).toEqual(true); + expect((): Uint8Array => + aliceSR25519KeyPair.encrypt(message, bobSR25519KeyPair.publicKey) + ).toThrow('Cannot encrypt with a locked key pair'); + }); + describe('ethereum', (): void => { const PUBLICDERIVED = new Uint8Array([ 3, 129, 53, 27, 27, 70, 210, 96, diff --git a/packages/keyring/src/pair/index.ts b/packages/keyring/src/pair/index.ts index 1e050f052c..4b534f765f 100644 --- a/packages/keyring/src/pair/index.ts +++ b/packages/keyring/src/pair/index.ts @@ -7,7 +7,7 @@ import type { KeyringPair, KeyringPair$Json, KeyringPair$Meta, SignOptions } fro import type { PairInfo } from './types'; import { assert, objectSpread, u8aConcat, u8aEmpty, u8aEq, u8aToHex, u8aToU8a } from '@polkadot/util'; -import { blake2AsU8a, convertPublicKeyToCurve25519, convertSecretKeyToCurve25519, ed25519PairFromSeed as ed25519FromSeed, ed25519Sign, ethereumEncode, keccakAsU8a, keyExtractPath, keyFromPath, naclOpen, naclSeal, secp256k1Compress, secp256k1Expand, secp256k1PairFromSeed as secp256k1FromSeed, secp256k1Sign, signatureVerify, sr25519PairFromSeed as sr25519FromSeed, sr25519Sign, sr25519VrfSign, sr25519VrfVerify } from '@polkadot/util-crypto'; +import { blake2AsU8a, convertPublicKeyToCurve25519, convertSecretKeyToCurve25519, ed25519Decrypt, ed25519PairFromSeed as ed25519FromSeed, ed25519Sign, encrypt as cryptoEncrypt, ethereumEncode, keccakAsU8a, keyExtractPath, keyFromPath, naclOpen, naclSeal, secp256k1Compress, secp256k1Expand, secp256k1PairFromSeed as secp256k1FromSeed, secp256k1Sign, signatureVerify, sr25519Decrypt, sr25519PairFromSeed as sr25519FromSeed, sr25519Sign, sr25519VrfSign, sr25519VrfVerify } from '@polkadot/util-crypto'; import { decodePair } from './decode'; import { encodePair } from './encode'; @@ -144,6 +144,14 @@ export function createPair ({ toSS58, type }: Setup, { publicKey, secretKey }: P }, // eslint-disable-next-line sort-keys decodePkcs8, + decrypt: (encryptedMessage: HexString | string | Uint8Array): Uint8Array | null => { + assert(!isLocked(secretKey), 'Cannot decrypt with a locked key pair'); + assert(!['ecdsa', 'ethereum'].includes(type), 'Secp256k1 not supported yet'); + + return type === 'ed25519' + ? ed25519Decrypt(u8aToU8a(encryptedMessage), { publicKey, secretKey }) + : sr25519Decrypt(u8aToU8a(encryptedMessage), { publicKey, secretKey }); + }, decryptMessage: (encryptedMessageWithNonce: HexString | string | Uint8Array, senderPublicKey: HexString | string | Uint8Array): Uint8Array | null => { assert(!isLocked(secretKey), 'Cannot encrypt with a locked key pair'); assert(!['ecdsa', 'ethereum'].includes(type), 'Secp256k1 not supported yet'); @@ -169,6 +177,12 @@ export function createPair ({ toSS58, type }: Setup, { publicKey, secretKey }: P encodePkcs8: (passphrase?: string): Uint8Array => { return recode(passphrase); }, + encrypt: (message: HexString | string | Uint8Array, recipientPublicKey: HexString | string | Uint8Array): Uint8Array => { + assert(!isLocked(secretKey), 'Cannot encrypt with a locked key pair'); + assert(!['ecdsa', 'ethereum'].includes(type), 'Secp256k1 not supported yet'); + + return cryptoEncrypt(message, recipientPublicKey, type, { publicKey, secretKey }); + }, encryptMessage: (message: HexString | string | Uint8Array, recipientPublicKey: HexString | string | Uint8Array, nonceIn?: Uint8Array): Uint8Array => { assert(!isLocked(secretKey), 'Cannot encrypt with a locked key pair'); assert(!['ecdsa', 'ethereum'].includes(type), 'Secp256k1 not supported yet'); diff --git a/packages/keyring/src/pair/nobody.ts b/packages/keyring/src/pair/nobody.ts index 1301becd8c..972ba55d67 100644 --- a/packages/keyring/src/pair/nobody.ts +++ b/packages/keyring/src/pair/nobody.ts @@ -31,6 +31,9 @@ const pair: KeyringPair = { decodePkcs8: (passphrase?: string, encoded?: Uint8Array): void => undefined, // eslint-disable-next-line @typescript-eslint/no-unused-vars + decrypt: (encryptedMessage: string | Uint8Array): Uint8Array | null => + null, + // eslint-disable-next-line @typescript-eslint/no-unused-vars decryptMessage: (encryptedMessageWithNonce: string | Uint8Array, senderPublicKey: string | Uint8Array): Uint8Array | null => null, // eslint-disable-next-line @typescript-eslint/no-unused-vars @@ -40,6 +43,9 @@ const pair: KeyringPair = { encodePkcs8: (passphrase?: string): Uint8Array => new Uint8Array(0), // eslint-disable-next-line @typescript-eslint/no-unused-vars + encrypt: (message: string | Uint8Array): Uint8Array => + new Uint8Array(), + // eslint-disable-next-line @typescript-eslint/no-unused-vars encryptMessage: (message: string | Uint8Array, recipientPublicKey: string | Uint8Array, _nonce?: Uint8Array): Uint8Array => new Uint8Array(), isLocked: true, diff --git a/packages/keyring/src/types.ts b/packages/keyring/src/types.ts index a410f06d65..90324e4562 100644 --- a/packages/keyring/src/types.ts +++ b/packages/keyring/src/types.ts @@ -41,6 +41,8 @@ export interface KeyringPair { verify (message: HexString | string | Uint8Array, signature: Uint8Array, signerPublic: HexString | string | Uint8Array): boolean; vrfSign (message: HexString | string | Uint8Array, context?: HexString | string | Uint8Array, extra?: HexString | string | Uint8Array): Uint8Array; vrfVerify (message: HexString | string | Uint8Array, vrfResult: Uint8Array, signerPublic: HexString | Uint8Array | string, context?: HexString | string | Uint8Array, extra?: HexString | string | Uint8Array): boolean; + encrypt (message: HexString | string | Uint8Array, recipientPublicKey: HexString | string | Uint8Array): Uint8Array; + decrypt (encryptedMessage: HexString | string | Uint8Array): Uint8Array | null; } export interface KeyringPairs { @@ -74,4 +76,5 @@ export interface KeyringInstance { getPublicKeys (): Uint8Array[]; removePair (address: string | Uint8Array): void; toJson (address: string | Uint8Array, passphrase?: string): KeyringPair$Json; + encrypt (message: string | Uint8Array, recipientPublicKey: string | Uint8Array, recipientKeyType?: KeypairType): Uint8Array; } diff --git a/packages/util-crypto/src/bundle.ts b/packages/util-crypto/src/bundle.ts index 2960c7822b..4acffd953f 100644 --- a/packages/util-crypto/src/bundle.ts +++ b/packages/util-crypto/src/bundle.ts @@ -13,6 +13,7 @@ export * from './base64'; export * from './blake2'; export * from './crypto'; export * from './ed25519'; +export * from './encrypt'; export * from './ethereum'; export * from './hd'; export * from './hmac'; diff --git a/packages/util-crypto/src/ed25519/decrypt.ts b/packages/util-crypto/src/ed25519/decrypt.ts new file mode 100644 index 0000000000..1fb9a771b7 --- /dev/null +++ b/packages/util-crypto/src/ed25519/decrypt.ts @@ -0,0 +1,44 @@ +// Copyright 2017-2021 @polkadot/util-crypto authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { HexString } from '@polkadot/util/types'; +import type { Keypair } from '../types'; + +import nacl from 'tweetnacl'; + +import { assert, u8aToU8a } from '@polkadot/util'; + +import { naclOpen } from '../nacl'; +import { convertPublicKeyToCurve25519, convertSecretKeyToCurve25519 } from './convertKey'; + +interface ed25519EncryptedMessage { + ephemeralPublicKey: Uint8Array; + nonce: Uint8Array; + sealed: Uint8Array; +} + +/** + * @name ed25519Decrypt + * @description Returns decrypted message of `encryptedMessage`, using the supplied pair + */ +export function ed25519Decrypt (encryptedMessage: HexString | Uint8Array | string, { secretKey }: Partial): Uint8Array | null { + const decapsulatedEncryptedMessage = ed25519DecapsulateEncryptedMessage(encryptedMessage); + const x25519PublicKey = convertPublicKeyToCurve25519(decapsulatedEncryptedMessage.ephemeralPublicKey); + const x25519SecretKey = convertSecretKeyToCurve25519(u8aToU8a(secretKey)); + + return naclOpen(decapsulatedEncryptedMessage.sealed, decapsulatedEncryptedMessage.nonce, x25519PublicKey, x25519SecretKey); +} + +/** + * @name ed25519DecapsulateEncryptedMessage + * @description Split raw encrypted message + */ +function ed25519DecapsulateEncryptedMessage (encryptedMessage: HexString | Uint8Array | string): ed25519EncryptedMessage { + assert(encryptedMessage.length > nacl.box.publicKeyLength + nacl.box.nonceLength + nacl.box.overheadLength, 'Too short encrypted message'); + + return { + ephemeralPublicKey: u8aToU8a(encryptedMessage.slice(nacl.box.nonceLength, nacl.box.nonceLength + nacl.box.publicKeyLength)), + nonce: u8aToU8a(encryptedMessage.slice(0, nacl.box.nonceLength)), + sealed: u8aToU8a(encryptedMessage.slice(nacl.box.nonceLength + nacl.box.publicKeyLength)) + }; +} diff --git a/packages/util-crypto/src/ed25519/encrypt.ts b/packages/util-crypto/src/ed25519/encrypt.ts new file mode 100644 index 0000000000..954aa35be1 --- /dev/null +++ b/packages/util-crypto/src/ed25519/encrypt.ts @@ -0,0 +1,24 @@ +// Copyright 2017-2021 @polkadot/util-crypto authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { HexString } from '@polkadot/util/types'; + +import { u8aConcat, u8aToU8a } from '@polkadot/util'; + +import { naclSeal } from '../nacl/seal'; +import { Keypair } from '../types'; +import { ed25519PairFromRandom } from './pair/fromRandom'; +import { convertPublicKeyToCurve25519, convertSecretKeyToCurve25519 } from './convertKey'; + +/** + * @name ed25519Encrypt + * @description Returns encrypted message of `message`, using the supplied pair + */ +export function ed25519Encrypt (message: HexString | Uint8Array | string, receiverPublicKey: Uint8Array, senderKeyPair?: Keypair): Uint8Array { + const messageKeyPair = senderKeyPair || ed25519PairFromRandom(); + const x25519PublicKey = convertPublicKeyToCurve25519(receiverPublicKey); + const x25519SecretKey = convertSecretKeyToCurve25519(messageKeyPair.secretKey); + const { nonce, sealed } = naclSeal(u8aToU8a(message), x25519SecretKey, x25519PublicKey); + + return u8aConcat(nonce, messageKeyPair.publicKey, sealed); +} diff --git a/packages/util-crypto/src/ed25519/index.ts b/packages/util-crypto/src/ed25519/index.ts index cec2714647..2e8af6c0fc 100644 --- a/packages/util-crypto/src/ed25519/index.ts +++ b/packages/util-crypto/src/ed25519/index.ts @@ -12,3 +12,5 @@ export { ed25519PairFromSeed } from './pair/fromSeed'; export { ed25519PairFromString } from './pair/fromString'; export { ed25519Sign } from './sign'; export { ed25519Verify } from './verify'; +export { ed25519Encrypt } from './encrypt'; +export { ed25519Decrypt } from './decrypt'; diff --git a/packages/util-crypto/src/encrypt/encrypt.ts b/packages/util-crypto/src/encrypt/encrypt.ts new file mode 100644 index 0000000000..f860137abd --- /dev/null +++ b/packages/util-crypto/src/encrypt/encrypt.ts @@ -0,0 +1,25 @@ +// Copyright 2017-2021 @polkadot/util-crypto authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { HexString } from '@polkadot/util/types'; +import type { Keypair, KeypairType } from '../types'; + +import { assert, u8aToU8a } from '@polkadot/util'; + +import { ed25519Encrypt } from '../ed25519'; +import { sr25519Encrypt } from '../sr25519'; + +/** + * @name encrypt + * @summary Encrypt a message using the given publickey + * @description Returns the encrypted message of the given message using the public key. + * The encrypted message can be decrypted by the corresponding keypair using keypair.decrypt() method + */ +export function encrypt (message: HexString | string | Uint8Array, recipientPublicKey: HexString | string | Uint8Array, recipientKeyType: KeypairType, senderKeyPair?: Keypair): Uint8Array { + assert(!['ecdsa', 'ethereum'].includes(recipientKeyType), 'Secp256k1 not supported yet'); + const publicKey = u8aToU8a(recipientPublicKey); + + return recipientKeyType === 'ed25519' + ? ed25519Encrypt(message, publicKey, senderKeyPair) + : sr25519Encrypt(message, publicKey, senderKeyPair); +} diff --git a/packages/util-crypto/src/encrypt/index.ts b/packages/util-crypto/src/encrypt/index.ts new file mode 100644 index 0000000000..5476f7a607 --- /dev/null +++ b/packages/util-crypto/src/encrypt/index.ts @@ -0,0 +1,4 @@ +// Copyright 2017-2021 @polkadot/util-crypto authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +export { encrypt } from './encrypt'; diff --git a/packages/util-crypto/src/sr25519/decrypt.ts b/packages/util-crypto/src/sr25519/decrypt.ts new file mode 100644 index 0000000000..323389298b --- /dev/null +++ b/packages/util-crypto/src/sr25519/decrypt.ts @@ -0,0 +1,54 @@ +// Copyright 2017-2021 @polkadot/util-crypto authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { HexString } from '@polkadot/util/types'; +import type { Keypair } from '../types'; + +import { assert, u8aCmp, u8aToU8a } from '@polkadot/util'; + +import { naclDecrypt } from '../nacl'; +import { buildSR25519EncryptionKey, keyDerivationSaltSize, macData, nonceSize } from './encrypt'; + +const publicKeySize = 32; +const macValueSize = 32; + +interface sr25519EncryptedMessage { + ephemeralPublicKey: Uint8Array; + keyDerivationSalt: Uint8Array; + macValue: Uint8Array; + nonce: Uint8Array; + sealed: Uint8Array; +} + +/** + * @name sr25519Decrypt + * @description Returns decrypted message of `encryptedMessage`, using the supplied pair + */ +export function sr25519Decrypt (encryptedMessage: HexString | Uint8Array | string, { secretKey }: Partial): Uint8Array | null { + const { ephemeralPublicKey, keyDerivationSalt, macValue, nonce, sealed } = sr25519DecapsulateEncryptedMessage(u8aToU8a(encryptedMessage)); + const { encryptionKey, macKey } = buildSR25519EncryptionKey(ephemeralPublicKey, + u8aToU8a(secretKey), + ephemeralPublicKey, + keyDerivationSalt); + const decryptedMacValue = macData(nonce, sealed, ephemeralPublicKey, macKey); + + assert(u8aCmp(decryptedMacValue, macValue) === 0, "Mac values don't match"); + + return naclDecrypt(sealed, nonce, encryptionKey); +} + +/** + * @name sr25519DecapsulateEncryptedMessage + * @description Split raw encrypted message + */ +function sr25519DecapsulateEncryptedMessage (encryptedMessage: Uint8Array): sr25519EncryptedMessage { + assert(encryptedMessage.byteLength > nonceSize + keyDerivationSaltSize + publicKeySize + macValueSize, 'Wrong encrypted message length'); + + return { + ephemeralPublicKey: encryptedMessage.slice(nonceSize + keyDerivationSaltSize, nonceSize + keyDerivationSaltSize + publicKeySize), + keyDerivationSalt: encryptedMessage.slice(nonceSize, nonceSize + keyDerivationSaltSize), + macValue: encryptedMessage.slice(nonceSize + keyDerivationSaltSize + publicKeySize, nonceSize + keyDerivationSaltSize + publicKeySize + macValueSize), + nonce: encryptedMessage.slice(0, nonceSize), + sealed: encryptedMessage.slice(nonceSize + keyDerivationSaltSize + publicKeySize + macValueSize) + }; +} diff --git a/packages/util-crypto/src/sr25519/encrypt.ts b/packages/util-crypto/src/sr25519/encrypt.ts new file mode 100644 index 0000000000..c6828ced53 --- /dev/null +++ b/packages/util-crypto/src/sr25519/encrypt.ts @@ -0,0 +1,100 @@ +// Copyright 2017-2021 @polkadot/util-crypto authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +// SR25519 Encryption/Decryption following Elliptic Curve Integrated Encryption Scheme (ECIES) +// https://cryptobook.nakov.com/asymmetric-key-ciphers/ecies-public-key-encryption +// Implementation details: +// The algorithms used were chosen among those already used in this library +// - 1 - Ephemeral Key generation +// Generate new keypair using the wasm sr25519KeypairFromSeed function, with a random seed from +// mnemonicGenerate +// - 2 - Key Agreement +// Use wasm sr25519Agree function between the generated ephemeral private key and the recipient public key +// - 3 - Key Derivation +// Use pbkdf2 (random salt is generated, default 2048 rounds) to derive a new secret from the previous step output +// The derived secret is split into : +// - MAC key (first 32 bytes) +// - encryption key (last 32 bytes) +// - 4 - Encryption +// Use nacl.secretbox api symmetric encryption (xsalsa20-poly1305) to encrypt the message +// with the encryption key generated at step 3. +// A nonce (24 bytes) is randomly generated. +// - 5 - MAC Generation +// HMAC SHA256 (using the MAC key from step 3) of the concatenation of the encryption nonce, ephemeral public key and encrypted message +// +// The encrypted message is the concatenation of the following elements : +// - nonce (24 bytes) : random generated nonce used for the symmetric encryption (step 4) +// - keyDerivationSalt (32 bytes) : random generated salt used for the key derivation (step 3) +// - public key (32 bytes): public key of the ephemeral generated keypair (step 1) +// - macValue (32 bytes): mac value computed at step 5 +// - encrypted (remaining bytes): encrypted message (step 4) + +import type { HexString } from '@polkadot/util/types'; + +import { assert, u8aConcat, u8aToU8a } from '@polkadot/util'; + +import { hmacSha256AsU8a } from '../hmac'; +import { mnemonicGenerate, mnemonicToMiniSecret } from '../mnemonic'; +import { naclEncrypt } from '../nacl'; +import { pbkdf2Encode } from '../pbkdf2'; +import { randomAsU8a } from '../random'; +import { Keypair } from '../types'; +import { sr25519PairFromSeed } from './pair/fromSeed'; +import { sr25519Agreement } from './agreement'; + +const encryptionKeySize = 32; +const macKeySize = 32; +const derivationKeyRounds = 2048; + +export const keyDerivationSaltSize = 32; +export const nonceSize = 24; + +/** + * @name sr25519Encrypt + * @description Returns encrypted message of `message`, using the supplied pair + */ +export function sr25519Encrypt (message: HexString | Uint8Array | string, receiverPublicKey: Uint8Array, senderKeyPair?: Keypair): Uint8Array { + const messageKeyPair = senderKeyPair || generateEphemeralKeypair(); + const { encryptionKey, keyDerivationSalt, macKey } = generateEncryptionKey(messageKeyPair, receiverPublicKey); + const { encrypted, nonce } = naclEncrypt(u8aToU8a(message), encryptionKey, randomAsU8a(nonceSize)); + const macValue = macData(nonce, encrypted, messageKeyPair.publicKey, macKey); + + return u8aConcat(nonce, keyDerivationSalt, messageKeyPair.publicKey, macValue, encrypted); +} + +function generateEphemeralKeypair (): Keypair { + return sr25519PairFromSeed(mnemonicToMiniSecret(mnemonicGenerate())); +} + +function generateEncryptionKey (senderKeyPair: Keypair, receiverPublicKey: Uint8Array) { + const { encryptionKey, keyDerivationSalt, macKey } = buildSR25519EncryptionKey(receiverPublicKey, senderKeyPair.secretKey, senderKeyPair.publicKey); + + return { + encryptionKey, + keyDerivationSalt, + macKey + }; +} + +export function buildSR25519EncryptionKey (publicKey: Uint8Array, secretKey: Uint8Array, encryptedMessagePairPublicKey: Uint8Array, salt: Uint8Array = randomAsU8a(keyDerivationSaltSize)) { + const agreementKey = sr25519Agreement(secretKey, publicKey); + const masterSecret = u8aConcat(encryptedMessagePairPublicKey, agreementKey); + + return deriveKey(masterSecret, salt); +} + +function deriveKey (masterSecret: Uint8Array, salt: Uint8Array) { + const { password } = pbkdf2Encode(masterSecret, salt, derivationKeyRounds); + + assert(password.byteLength >= macKeySize + encryptionKeySize, 'Wrong derived key length'); + + return { + encryptionKey: password.slice(macKeySize, macKeySize + encryptionKeySize), + keyDerivationSalt: salt, + macKey: password.slice(0, macKeySize) + }; +} + +export function macData (nonce: Uint8Array, encryptedMessage: Uint8Array, encryptedMessagePairPublicKey: Uint8Array, macKey: Uint8Array): Uint8Array { + return hmacSha256AsU8a(macKey, u8aConcat(nonce, encryptedMessagePairPublicKey, encryptedMessage)); +} diff --git a/packages/util-crypto/src/sr25519/index.ts b/packages/util-crypto/src/sr25519/index.ts index b0d14e2f5d..1d4194a6a4 100644 --- a/packages/util-crypto/src/sr25519/index.ts +++ b/packages/util-crypto/src/sr25519/index.ts @@ -10,3 +10,5 @@ export { sr25519Sign } from './sign'; export { sr25519Verify } from './verify'; export { sr25519VrfSign } from './vrfSign'; export { sr25519VrfVerify } from './vrfVerify'; +export { sr25519Encrypt } from './encrypt'; +export { sr25519Decrypt } from './decrypt';