Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

Asymmetric encryption/decryption feature #1331

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
12 changes: 11 additions & 1 deletion packages/keyring/src/keyring.ts
Expand Up @@ -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';
Expand Down Expand Up @@ -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);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Generally users have no idea even what their own keypair types are, let alone somebody else knowing what a specific user has.

This means that the addition of the recipientKeyType here is an issue - the sender won't know which type the actual recipient uses. It could be anything and for most users this is not known at all.

Copy link
Author

@LaurentTrk LaurentTrk Jul 4, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @jacogr,
Thanks for your review.
I actually did not catch your point here, the users of this library are not end users, I guess that they are more techy (as developers) and aware of what key types and encryption are.
I agree that passing the wrong key type for a given public key will fail, but we need this information.

In case the user don't know which type to use, the keyring will use its default value (the one used when creating new key pairs)

}
}
48 changes: 47 additions & 1 deletion packages/keyring/src/pair/index.spec.ts
Expand Up @@ -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);
Expand Down Expand Up @@ -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");
});
Comment on lines +246 to +254
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We would need an additional test (on the back of the above above comment), where the one party uses ed25519 and the other party using sr25519 and they can exchange messages bi-directionally.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test has not been done as you cannot exchange messages if the 2 key types differs.


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,
Expand Down
16 changes: 15 additions & 1 deletion packages/keyring/src/pair/index.ts
Expand Up @@ -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';
Expand Down Expand Up @@ -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 });

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In above 2 lines, no need to pass-in the public key, only the secret key is needed to decrypt the message.

},
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');
Expand All @@ -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');
Expand Down
6 changes: 6 additions & 0 deletions packages/keyring/src/pair/nobody.ts
Expand Up @@ -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
Expand All @@ -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,
Expand Down
3 changes: 3 additions & 0 deletions packages/keyring/src/types.ts
Expand Up @@ -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 {
Expand Down Expand Up @@ -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;
}
1 change: 1 addition & 0 deletions packages/util-crypto/src/bundle.ts
Expand Up @@ -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';
Expand Down
44 changes: 44 additions & 0 deletions 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<Keypair>): 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))
};
}
24 changes: 24 additions & 0 deletions 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);
}
2 changes: 2 additions & 0 deletions packages/util-crypto/src/ed25519/index.ts
Expand Up @@ -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';
25 changes: 25 additions & 0 deletions 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);
}
4 changes: 4 additions & 0 deletions 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';
54 changes: 54 additions & 0 deletions 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<Keypair>): 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)
};
}