diff --git a/src/api/index.ts b/src/api/index.ts index d576089..2a90dcb 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -7,7 +7,7 @@ import { sleep, waitForFinality, Wallet_ } from '../utils'; import { ENCRYPTION_OVERHEAD_BYTES } from '../utils/ecdh-encryption'; import { CyclicByteBuffer } from '../utils/cyclic-bytebuffer'; import ByteBuffer from 'bytebuffer'; -import { TextSerdeFactory } from './text-serde'; +import { EncryptionProps, TextSerdeFactory } from './text-serde'; // TODO: Switch from types to classes @@ -254,9 +254,9 @@ export async function getDialectProgramAddress( function parseMessages( { messages: rawMessagesBuffer, members, encrypted }: RawDialect, - user?: anchor.web3.Keypair, + encryptionProps?: EncryptionProps, ) { - if (encrypted && !user) { + if (encrypted && !encryptionProps) { return []; } const messagesBuffer = new CyclicByteBuffer( @@ -270,7 +270,7 @@ function parseMessages( encrypted, members, }, - user, + encryptionProps, ); const allMessages: Message[] = messagesBuffer.items().map(({ buffer }) => { const byteBuffer = new ByteBuffer(buffer.length).append(buffer).flip(); @@ -288,29 +288,29 @@ function parseMessages( return allMessages.reverse(); } -function parseRawDialect(rawDialect: RawDialect, user?: anchor.web3.Keypair) { +function parseRawDialect( + rawDialect: RawDialect, + encryptionProps?: EncryptionProps, +) { return { encrypted: rawDialect.encrypted, members: rawDialect.members, nextMessageIdx: rawDialect.messages.writeOffset, lastMessageTimestamp: rawDialect.lastMessageTimestamp * 1000, - messages: parseMessages(rawDialect, user), + messages: parseMessages(rawDialect, encryptionProps), }; } export async function getDialect( program: anchor.Program, publicKey: PublicKey, - user?: anchor.web3.Keypair | Wallet, + encryptionProps?: EncryptionProps, ): Promise { const rawDialect = (await program.account.dialectAccount.fetch( publicKey, )) as RawDialect; const account = await program.provider.connection.getAccountInfo(publicKey); - const dialect = parseRawDialect( - rawDialect, - user && 'secretKey' in user ? user : undefined, - ); + const dialect = parseRawDialect(rawDialect, encryptionProps); return { ...account, publicKey: publicKey, @@ -321,6 +321,7 @@ export async function getDialect( export async function getDialects( program: anchor.Program, user: anchor.web3.Keypair | Wallet, + encryptionProps?: EncryptionProps, ): Promise { const metadata = await getMetadata(program, user.publicKey); const enabledSubscriptions = metadata.subscriptions.filter( @@ -328,7 +329,7 @@ export async function getDialects( ); return Promise.all( enabledSubscriptions.map(async ({ pubkey }) => - getDialect(program, pubkey, user), + getDialect(program, pubkey, encryptionProps), ), ).then((dialects) => dialects.sort( @@ -341,13 +342,13 @@ export async function getDialects( export async function getDialectForMembers( program: anchor.Program, members: Member[], - user?: anchor.web3.Keypair, + encryptionProps?: EncryptionProps, ): Promise { const sortedMembers = members.sort((a, b) => a.publicKey.toBuffer().compare(b.publicKey.toBuffer()), ); const [publicKey] = await getDialectProgramAddress(program, sortedMembers); - return await getDialect(program, publicKey, user); + return await getDialect(program, publicKey, encryptionProps); } export async function findDialects( @@ -395,7 +396,8 @@ export async function createDialect( program: anchor.Program, owner: anchor.web3.Keypair | Wallet, members: Member[], - encrypted = true, + encrypted = false, + encryptionProps?: EncryptionProps, ): Promise { const sortedMembers = members.sort((a, b) => a.publicKey.toBuffer().compare(b.publicKey.toBuffer()), @@ -425,11 +427,7 @@ export async function createDialect( }, ); await waitForFinality(program, tx); - return await getDialectForMembers( - program, - members, - 'secretKey' in owner ? owner : undefined, - ); + return await getDialectForMembers(program, members, encryptionProps); } export async function deleteDialect( @@ -470,6 +468,7 @@ export async function sendMessage( { dialect, publicKey }: DialectAccount, sender: anchor.web3.Keypair | Wallet, text: string, + encryptionProps?: EncryptionProps, ): Promise { const [dialectPublicKey, nonce] = await getDialectProgramAddress( program, @@ -480,7 +479,7 @@ export async function sendMessage( encrypted: dialect.encrypted, members: dialect.members, }, - sender && 'secretKey' in sender ? sender : undefined, + encryptionProps, ); const serializedText = textSerde.serialize(text); await program.rpc.sendMessage( @@ -498,7 +497,7 @@ export async function sendMessage( signers: sender && 'secretKey' in sender ? [sender] : [], }, ); - const d = await getDialect(program, publicKey, sender); + const d = await getDialect(program, publicKey, encryptionProps); return d.dialect.messages[d.dialect.nextMessageIdx - 1]; // TODO: Support ring } diff --git a/src/api/text-serde.ts b/src/api/text-serde.ts index 8525020..fff8dda 100644 --- a/src/api/text-serde.ts +++ b/src/api/text-serde.ts @@ -3,8 +3,14 @@ import { generateRandomNonceWithPrefix, NONCE_SIZE_BYTES, } from '../utils/nonce-generator'; -import { ecdhDecrypt, ecdhEncrypt } from '../utils/ecdh-encryption'; +import { + Curve25519KeyPair, + ecdhDecrypt, + ecdhEncrypt, + Ed25519Key, +} from '../utils/ecdh-encryption'; import * as anchor from '@project-serum/anchor'; +import { PublicKey } from '@solana/web3.js'; export interface TextSerde { serialize(text: string): Uint8Array; @@ -17,37 +23,35 @@ export class EncryptedTextSerde implements TextSerde { new UnencryptedTextSerde(); constructor( - private readonly user: anchor.web3.Keypair, + private readonly encryptionProps: EncryptionProps, private readonly members: Member[], ) {} deserialize(bytes: Uint8Array): string { const encryptionNonce = bytes.slice(0, NONCE_SIZE_BYTES); const encryptedText = bytes.slice(NONCE_SIZE_BYTES, bytes.length); - const otherMember = this.findOtherMember(this.user.publicKey); + const otherMember = this.findOtherMember( + new PublicKey(this.encryptionProps.ed25519PublicKey), + ); const encodedText = ecdhDecrypt( encryptedText, - { - secretKey: this.user.secretKey, - publicKey: this.user.publicKey.toBytes(), - }, - otherMember.publicKey.toBuffer(), + this.encryptionProps.diffieHellmanKeyPair, + otherMember.publicKey.toBytes(), encryptionNonce, ); return this.unencryptedTextSerde.deserialize(encodedText); } serialize(text: string): Uint8Array { - const senderMemberIdx = this.findMemberIdx(this.user.publicKey); + const publicKey = new PublicKey(this.encryptionProps.ed25519PublicKey); + const senderMemberIdx = this.findMemberIdx(publicKey); const textBytes = this.unencryptedTextSerde.serialize(text); - const otherMember = this.findOtherMember(this.user.publicKey); + const otherMember = this.findOtherMember(publicKey); const encryptionNonce = generateRandomNonceWithPrefix(senderMemberIdx); const encryptedText = ecdhEncrypt( textBytes, - { - secretKey: this.user.secretKey, - publicKey: this.user.publicKey.toBytes(), - }, + this.encryptionProps.diffieHellmanKeyPair, + otherMember.publicKey.toBytes(), encryptionNonce, ); @@ -88,17 +92,22 @@ export type DialectAttributes = { members: Member[]; }; +export interface EncryptionProps { + diffieHellmanKeyPair: Curve25519KeyPair; + ed25519PublicKey: Ed25519Key; +} + export class TextSerdeFactory { static create( { encrypted, members }: DialectAttributes, - user?: anchor.web3.Keypair, + encryptionProps?: EncryptionProps, ): TextSerde { if (!encrypted) { return new UnencryptedTextSerde(); } - if (encrypted && user) { - return new EncryptedTextSerde(user, members); + if (encrypted && encryptionProps) { + return new EncryptedTextSerde(encryptionProps, members); } - throw new Error('Cannot proceed with encrypted dialect w/o user identity'); + throw new Error('Cannot proceed without encryptionProps'); } } diff --git a/src/utils/ecdh-encryption.spec.ts b/src/utils/ecdh-encryption.spec.ts index 6b23534..43d8117 100644 --- a/src/utils/ecdh-encryption.spec.ts +++ b/src/utils/ecdh-encryption.spec.ts @@ -1,12 +1,29 @@ import { expect } from 'chai'; import { + Curve25519KeyPair, ecdhDecrypt, ecdhEncrypt, ENCRYPTION_OVERHEAD_BYTES, - generateEd25519KeyPair, } from './ecdh-encryption'; import { randomBytes } from 'tweetnacl'; import { NONCE_SIZE_BYTES } from './nonce-generator'; +import { Keypair } from '@solana/web3.js'; +import ed2curve from 'ed2curve'; + +function generateKeypair() { + const { publicKey, secretKey } = new Keypair(); + const curve25519: Curve25519KeyPair = ed2curve.convertKeyPair({ + publicKey: publicKey.toBytes(), + secretKey, + })!; + return { + ed25519: { + publicKey: publicKey.toBytes(), + secretKey, + }, + curve25519, + }; +} describe('ECDH encryptor/decryptor test', async () => { /* @@ -25,10 +42,13 @@ describe('ECDH encryptor/decryptor test', async () => { const sizesComparison = messageSizes.map((size) => { const unencrypted = randomBytes(size); const nonce = randomBytes(NONCE_SIZE_BYTES); + const keyPair1 = generateKeypair(); + const keyPair2 = generateKeypair(); + const encrypted = ecdhEncrypt( unencrypted, - generateEd25519KeyPair(), - generateEd25519KeyPair().publicKey, + keyPair1.curve25519, + keyPair2.ed25519.publicKey, nonce, ); return { @@ -47,19 +67,19 @@ describe('ECDH encryptor/decryptor test', async () => { // given const unencrypted = randomBytes(10); const nonce = randomBytes(NONCE_SIZE_BYTES); - const party1KeyPair = generateEd25519KeyPair(); - const party2KeyPair = generateEd25519KeyPair(); + const party1KeyPair = generateKeypair(); + const party2KeyPair = generateKeypair(); const encrypted = ecdhEncrypt( unencrypted, - party1KeyPair, - party2KeyPair.publicKey, + party1KeyPair.curve25519, + party2KeyPair.ed25519.publicKey, nonce, ); // when const decrypted = ecdhDecrypt( encrypted, - party1KeyPair, - party2KeyPair.publicKey, + party1KeyPair.curve25519, + party2KeyPair.ed25519.publicKey, nonce, ); // then @@ -71,19 +91,19 @@ describe('ECDH encryptor/decryptor test', async () => { // given const unencrypted = randomBytes(10); const nonce = randomBytes(NONCE_SIZE_BYTES); - const party1KeyPair = generateEd25519KeyPair(); - const party2KeyPair = generateEd25519KeyPair(); + const party1KeyPair = generateKeypair(); + const party2KeyPair = generateKeypair(); const encrypted = ecdhEncrypt( unencrypted, - party1KeyPair, - party2KeyPair.publicKey, + party1KeyPair.curve25519, + party2KeyPair.ed25519.publicKey, nonce, ); // when const decrypted = ecdhDecrypt( encrypted, - party2KeyPair, - party1KeyPair.publicKey, + party2KeyPair.curve25519, + party1KeyPair.ed25519.publicKey, nonce, ); // then diff --git a/src/utils/ecdh-encryption.ts b/src/utils/ecdh-encryption.ts index 0836901..bf5cf3f 100644 --- a/src/utils/ecdh-encryption.ts +++ b/src/utils/ecdh-encryption.ts @@ -1,12 +1,11 @@ import ed2curve from 'ed2curve'; import nacl from 'tweetnacl'; -import { Keypair } from '@solana/web3.js'; export const ENCRYPTION_OVERHEAD_BYTES = 16; export class IncorrectPublicKeyFormatError extends Error { - constructor(party: string) { - super(`Given '${party}' public key is not valid Ed25519 key`); + constructor() { + super('IncorrectPublicKeyFormatError'); } } @@ -16,70 +15,67 @@ export class AuthenticationFailedError extends Error { } } +export type Curve25519Key = Uint8Array; + +export type Curve25519KeyPair = { + publicKey: Curve25519Key; + secretKey: Curve25519Key; +}; + export type Ed25519Key = Uint8Array; export type Ed25519KeyPair = { - publicKey: Ed25519Key; - secretKey: Ed25519Key; + publicKey: Curve25519Key; + secretKey: Curve25519Key; }; -export function generateEd25519KeyPair(): Ed25519KeyPair { - const keypair = Keypair.generate(); - return { - publicKey: keypair.publicKey.toBytes(), - secretKey: keypair.secretKey, - }; -} - -export function ecdhEncrypt( - payload: Uint8Array, - { secretKey, publicKey }: Ed25519KeyPair, - otherPartyPublicKey: Ed25519Key, - nonce: Uint8Array, -): Uint8Array { +export function ed25519KeyPairToCurve25519({ + publicKey, + secretKey, +}: Ed25519KeyPair): Curve25519KeyPair { const curve25519KeyPair = ed2curve.convertKeyPair({ publicKey, secretKey, }); if (!curve25519KeyPair) { - throw new IncorrectPublicKeyFormatError('encryptor keypair'); + throw new IncorrectPublicKeyFormatError(); } - const otherPartyCurve25519PublicKey = - ed2curve.convertPublicKey(otherPartyPublicKey); - if (!otherPartyCurve25519PublicKey) { - throw new IncorrectPublicKeyFormatError('other party'); + return curve25519KeyPair; +} + +export function ed25519PublicKeyToCurve25519(key: Ed25519Key): Curve25519Key { + const curve25519PublicKey = ed2curve.convertPublicKey(key); + if (!curve25519PublicKey) { + throw new IncorrectPublicKeyFormatError(); } + return curve25519PublicKey; +} + +export function ecdhEncrypt( + payload: Uint8Array, + { secretKey, publicKey }: Curve25519KeyPair, + otherPartyPublicKey: Ed25519Key, + nonce: Uint8Array, +): Uint8Array { return nacl.box( payload, nonce, - otherPartyCurve25519PublicKey, - curve25519KeyPair.secretKey, + ed25519PublicKeyToCurve25519(otherPartyPublicKey), + secretKey, ); } export function ecdhDecrypt( payload: Uint8Array, - { secretKey, publicKey }: Ed25519KeyPair, + { secretKey, publicKey }: Curve25519KeyPair, otherPartyPublicKey: Ed25519Key, nonce: Uint8Array, ): Uint8Array { - const curve25519KeyPair = ed2curve.convertKeyPair({ - publicKey, - secretKey, - }); - if (!curve25519KeyPair) { - throw new IncorrectPublicKeyFormatError('decryptor keypair'); - } - const otherPartyCurve25519PublicKey = - ed2curve.convertPublicKey(otherPartyPublicKey); - if (!otherPartyCurve25519PublicKey) { - throw new IncorrectPublicKeyFormatError('other party'); - } const decrypted = nacl.box.open( payload, nonce, - otherPartyCurve25519PublicKey, - curve25519KeyPair.secretKey, + ed25519PublicKeyToCurve25519(otherPartyPublicKey), + secretKey, ); if (!decrypted) { throw new AuthenticationFailedError(); diff --git a/tests/test-v1.ts b/tests/test-v1.ts index 2b302af..8a782e7 100644 --- a/tests/test-v1.ts +++ b/tests/test-v1.ts @@ -23,10 +23,14 @@ import { } from '../src/api'; import { sleep } from '../src/utils'; import { ITEM_METADATA_OVERHEAD } from '../src/utils/cyclic-bytebuffer'; -import { ENCRYPTION_OVERHEAD_BYTES } from '../src/utils/ecdh-encryption'; +import { + ed25519KeyPairToCurve25519, + ENCRYPTION_OVERHEAD_BYTES, +} from '../src/utils/ecdh-encryption'; import { NONCE_SIZE_BYTES } from '../src/utils/nonce-generator'; import { randomInt } from 'crypto'; import { CountDownLatch } from '../src/utils/countdown-latch'; +import { EncryptionProps } from '../src/api/text-serde'; chai.use(chaiAsPromised); anchor.setProvider(anchor.Provider.local()); @@ -40,14 +44,18 @@ describe('Protocol v1 test', () => { let writer: web3.Keypair; beforeEach(async () => { - owner = await createUser({ - requestAirdrop: true, - createMeta: false, - }); - writer = await createUser({ - requestAirdrop: true, - createMeta: false, - }); + owner = ( + await createUser({ + requestAirdrop: true, + createMeta: false, + }) + ).user; + writer = ( + await createUser({ + requestAirdrop: true, + createMeta: false, + }) + ).user; }); it('Create user metadata object(s)', async () => { @@ -78,18 +86,24 @@ describe('Protocol v1 test', () => { let members: Member[] = []; beforeEach(async () => { - owner = await createUser({ - requestAirdrop: true, - createMeta: true, - }); - writer = await createUser({ - requestAirdrop: true, - createMeta: true, - }); - nonmember = await createUser({ - requestAirdrop: true, - createMeta: false, - }); + owner = ( + await createUser({ + requestAirdrop: true, + createMeta: true, + }) + ).user; + writer = ( + await createUser({ + requestAirdrop: true, + createMeta: true, + }) + ).user; + nonmember = ( + await createUser({ + requestAirdrop: true, + createMeta: false, + }) + ).user; members = [ { publicKey: owner.publicKey, @@ -187,9 +201,9 @@ describe('Protocol v1 test', () => { expect(dialectAccount.dialect.encrypted).to.be.false; }); - it('Creates encrypted dialect by default', async () => { + it('Creates unencrypted dialect by default', async () => { const dialectAccount = await createDialect(program, owner, members); - expect(dialectAccount.dialect.encrypted).to.be.true; + expect(dialectAccount.dialect.encrypted).to.be.false; }); it('Fail to create a second dialect for the same members', async () => { @@ -210,7 +224,7 @@ describe('Protocol v1 test', () => { it('Find a dialect for a given member pair, verify correct scopes.', async () => { await createDialect(program, owner, members); - const dialect = await getDialectForMembers(program, members, writer); + const dialect = await getDialectForMembers(program, members); members.every((m, i) => expect( m.publicKey.equals(dialect.dialect.members[i].publicKey) && @@ -252,15 +266,15 @@ describe('Protocol v1 test', () => { createUser({ requestAirdrop: true, createMeta: true, - }), + }).then((it) => it.user), createUser({ requestAirdrop: true, createMeta: true, - }), + }).then((it) => it.user), createUser({ requestAirdrop: true, createMeta: true, - }), + }).then((it) => it.user), ]); console.log('Creating dialects'); // create first dialect and subscribe users @@ -360,15 +374,15 @@ describe('Protocol v1 test', () => { createUser({ requestAirdrop: true, createMeta: false, - }), + }).then((it) => it.user), createUser({ requestAirdrop: true, createMeta: false, - }), + }).then((it) => it.user), createUser({ requestAirdrop: true, createMeta: false, - }), + }).then((it) => it.user), ]); const [user1User2Dialect, user1User3Dialect, user2User3Dialect] = await Promise.all([ @@ -454,34 +468,34 @@ describe('Protocol v1 test', () => { let dialect: DialectAccount; beforeEach(async () => { - owner = await createUser({ - requestAirdrop: true, - createMeta: true, - }); - writer = await createUser({ + (owner = await createUser({ requestAirdrop: true, createMeta: true, - }); - nonmember = await createUser({ - requestAirdrop: true, - createMeta: false, - }); - members = [ - { - publicKey: owner.publicKey, - scopes: [true, false], // owner, read-only - }, - { - publicKey: writer.publicKey, - scopes: [false, true], // non-owner, read-write - }, - ]; + }).then((it) => it.user)), + (writer = await createUser({ + requestAirdrop: true, + createMeta: true, + }).then((it) => it.user)), + (nonmember = await createUser({ + requestAirdrop: true, + createMeta: false, + }).then((it) => it.user)), + (members = [ + { + publicKey: owner.publicKey, + scopes: [true, false], // owner, read-only + }, + { + publicKey: writer.publicKey, + scopes: [false, true], // non-owner, read-write + }, + ]); dialect = await createDialect(program, owner, members, false); }); it('Message sender and receiver can read the message text and time', async () => { // given - const dialect = await getDialectForMembers(program, members, writer); + const dialect = await getDialectForMembers(program, members); const text = generateRandomText(256); // when await sendMessage(program, dialect, writer, text); @@ -489,7 +503,6 @@ describe('Protocol v1 test', () => { const senderDialect = await getDialectForMembers( program, dialect.dialect.members, - writer, ); const message = senderDialect.dialect.messages[0]; chai.expect(message.text).to.be.eq(text); @@ -498,36 +511,9 @@ describe('Protocol v1 test', () => { .to.be.eq(message.timestamp); }); - it('Non-member can read any of the messages', async () => { - // given - const senderDialect = await getDialectForMembers( - program, - members, - writer, - ); - const text = generateRandomText(256); - await sendMessage(program, senderDialect, writer, text); - // when / then - const nonMemberDialect = await getDialectForMembers( - program, - dialect.dialect.members, - nonmember, - ); - const message = nonMemberDialect.dialect.messages[0]; - chai.expect(message.text).to.be.eq(text); - chai.expect(message.owner).to.be.deep.eq(writer.publicKey); - chai - .expect(nonMemberDialect.dialect.lastMessageTimestamp) - .to.be.eq(message.timestamp); - }); - it('Anonymous user can read any of the messages', async () => { // given - const senderDialect = await getDialectForMembers( - program, - members, - writer, - ); + const senderDialect = await getDialectForMembers(program, members); const text = generateRandomText(256); await sendMessage(program, senderDialect, writer, text); // when / then @@ -563,7 +549,7 @@ describe('Protocol v1 test', () => { // verify last last N messages look correct const messageCounter = messageIdx + 1; const text = texts[messageIdx]; - const dialect = await getDialectForMembers(program, members, writer); + const dialect = await getDialectForMembers(program, members); console.log( `Sending message ${messageCounter}/${texts.length} len = ${text.length} @@ -580,7 +566,7 @@ describe('Protocol v1 test', () => { ); const sliceEnd = sliceStart + expectedMessagesCount; const expectedMessages = texts.slice(sliceStart, sliceEnd).reverse(); - const d = await getDialect(program, dialect.publicKey, writer); + const d = await getDialect(program, dialect.publicKey); const actualMessages = d.dialect.messages.map((m) => m.text); console.log(` msgs count after send: ${actualMessages.length}\n`); expect(actualMessages).to.be.deep.eq(expectedMessages); @@ -595,7 +581,7 @@ describe('Protocol v1 test', () => { for (let messageIdx = 0; messageIdx < texts.length; messageIdx++) { const text = texts[messageIdx]; const messageCounter = messageIdx + 1; - const dialect = await getDialectForMembers(program, members, writer); + const dialect = await getDialectForMembers(program, members); console.log( `Sending message ${messageCounter}/${texts.length} len = ${text.length} @@ -603,7 +589,7 @@ describe('Protocol v1 test', () => { ); // when await sendMessage(program, dialect, writer, text); - const d = await getDialect(program, dialect.publicKey, writer); + const d = await getDialect(program, dialect.publicKey); const actualMessages = d.dialect.messages; const lastMessage = actualMessages[0]; console.log(` msgs count after send: ${actualMessages.length}\n`); @@ -615,24 +601,33 @@ describe('Protocol v1 test', () => { describe('Encrypted messaging tests', () => { let owner: web3.Keypair; + let ownerEncryptionProps: EncryptionProps; let writer: web3.Keypair; + let writerEncryptionProps: EncryptionProps; let nonmember: web3.Keypair; + let nonmemberEncryptionProps: EncryptionProps; let members: Member[] = []; let dialect: DialectAccount; beforeEach(async () => { - owner = await createUser({ + const ownerUser = await createUser({ requestAirdrop: true, createMeta: true, }); - writer = await createUser({ + owner = ownerUser.user; + ownerEncryptionProps = ownerUser.encryptionProps; + const writerUser = await createUser({ requestAirdrop: true, createMeta: true, }); - nonmember = await createUser({ + writer = writerUser.user; + writerEncryptionProps = writerUser.encryptionProps; + const nonmemberUser = await createUser({ requestAirdrop: true, createMeta: false, }); + nonmember = nonmemberUser.user; + nonmemberEncryptionProps = nonmemberUser.encryptionProps; members = [ { publicKey: owner.publicKey, @@ -648,15 +643,19 @@ describe('Protocol v1 test', () => { it('Message sender can send msg and then read the message text and time', async () => { // given - const dialect = await getDialectForMembers(program, members, writer); + const dialect = await getDialectForMembers( + program, + members, + writerEncryptionProps, + ); const text = generateRandomText(256); // when - await sendMessage(program, dialect, writer, text); + await sendMessage(program, dialect, writer, text, writerEncryptionProps); // then const senderDialect = await getDialectForMembers( program, dialect.dialect.members, - writer, + writerEncryptionProps, ); const message = senderDialect.dialect.messages[0]; chai.expect(message.text).to.be.eq(text); @@ -671,16 +670,22 @@ describe('Protocol v1 test', () => { const senderDialect = await getDialectForMembers( program, members, - writer, + writerEncryptionProps, ); const text = generateRandomText(256); // when - await sendMessage(program, senderDialect, writer, text); + await sendMessage( + program, + senderDialect, + writer, + text, + writerEncryptionProps, + ); // then const receiverDialect = await getDialectForMembers( program, dialect.dialect.members, - owner, + ownerEncryptionProps, ); const message = receiverDialect.dialect.messages[0]; chai.expect(message.text).to.be.eq(text); @@ -695,13 +700,24 @@ describe('Protocol v1 test', () => { const senderDialect = await getDialectForMembers( program, members, - writer, + writerEncryptionProps, ); const text = generateRandomText(256); - await sendMessage(program, senderDialect, writer, text); + await sendMessage( + program, + senderDialect, + writer, + text, + writerEncryptionProps, + ); // when / then - expect(getDialectForMembers(program, dialect.dialect.members, nonmember)) - .to.eventually.be.rejected; + expect( + getDialectForMembers( + program, + dialect.dialect.members, + nonmemberEncryptionProps, + ), + ).to.eventually.be.rejected; }); it('New messages overwrite old, retrieved messages are in order.', async () => { @@ -728,13 +744,23 @@ describe('Protocol v1 test', () => { // verify last last N messages look correct const messageCounter = messageIdx + 1; const text = texts[messageIdx]; - const dialect = await getDialectForMembers(program, members, writer); + const dialect = await getDialectForMembers( + program, + members, + writerEncryptionProps, + ); console.log( `Sending message ${messageCounter}/${texts.length} len = ${text.length} idx: ${dialect.dialect.nextMessageIdx}`, ); - await sendMessage(program, dialect, writer, text); + await sendMessage( + program, + dialect, + writer, + text, + writerEncryptionProps, + ); const sliceStart = messageCounter <= messagesPerDialect ? 0 @@ -745,7 +771,11 @@ describe('Protocol v1 test', () => { ); const sliceEnd = sliceStart + expectedMessagesCount; const expectedMessages = texts.slice(sliceStart, sliceEnd).reverse(); - const d = await getDialect(program, dialect.publicKey, writer); + const d = await getDialect( + program, + dialect.publicKey, + writerEncryptionProps, + ); const actualMessages = d.dialect.messages.map((m) => m.text); console.log(` msgs count after send: ${actualMessages.length}\n`); expect(actualMessages).to.be.deep.eq(expectedMessages); @@ -759,15 +789,29 @@ describe('Protocol v1 test', () => { for (let messageIdx = 0; messageIdx < texts.length; messageIdx++) { const text = texts[messageIdx]; const messageCounter = messageIdx + 1; - const dialect = await getDialectForMembers(program, members, writer); + const dialect = await getDialectForMembers( + program, + members, + writerEncryptionProps, + ); console.log( `Sending message ${messageCounter}/${texts.length} len = ${text.length} idx: ${dialect.dialect.nextMessageIdx}`, ); // when - await sendMessage(program, dialect, writer, text); - const d = await getDialect(program, dialect.publicKey, writer); + await sendMessage( + program, + dialect, + writer, + text, + writerEncryptionProps, + ); + const d = await getDialect( + program, + dialect.publicKey, + writerEncryptionProps, + ); const actualMessages = d.dialect.messages; const lastMessage = actualMessages[0]; console.log(` msgs count after send: ${actualMessages.length}\n`); @@ -798,15 +842,29 @@ describe('Protocol v1 test', () => { for (let messageIdx = 0; messageIdx < texts.length; messageIdx++) { const text = texts[messageIdx]; const messageCounter = messageIdx + 1; - const dialect = await getDialectForMembers(program, members, writer); + const dialect = await getDialectForMembers( + program, + members, + writerEncryptionProps, + ); console.log( `Sending message ${messageCounter}/${texts.length} len = ${text.length} idx: ${dialect.dialect.nextMessageIdx}`, ); // when - await sendMessage(program, dialect, writer, text); - const d = await getDialect(program, dialect.publicKey, writer); + await sendMessage( + program, + dialect, + writer, + text, + writerEncryptionProps, + ); + const d = await getDialect( + program, + dialect.publicKey, + writerEncryptionProps, + ); const actualMessages = d.dialect.messages; const lastMessage = actualMessages[0]; console.log(` msgs count after send: ${actualMessages.length}\n`); @@ -827,48 +885,68 @@ describe('Protocol v1 test', () => { }); members = [ { - publicKey: writer1.publicKey, + publicKey: writer1.user.publicKey, scopes: [true, true], // owner, read-only }, { - publicKey: writer2.publicKey, + publicKey: writer2.user.publicKey, scopes: [false, true], // non-owner, read-write }, ]; - await createDialect(program, writer1, members, true); + await createDialect(program, writer1.user, members, true); // when let writer1Dialect = await getDialectForMembers( program, members, - writer1, + writer1.encryptionProps, ); const writer1Text = generateRandomText(256); - await sendMessage(program, writer1Dialect, writer1, writer1Text); + await sendMessage( + program, + writer1Dialect, + writer1.user, + writer1Text, + writer1.encryptionProps, + ); let writer2Dialect = await getDialectForMembers( program, members, - writer2, + writer2.encryptionProps, ); // ensures dialect state linearization const writer2Text = generateRandomText(256); - await sendMessage(program, writer2Dialect, writer2, writer2Text); + await sendMessage( + program, + writer2Dialect, + writer2.user, + writer2Text, + writer2.encryptionProps, + ); - writer1Dialect = await getDialectForMembers(program, members, writer1); - writer2Dialect = await getDialectForMembers(program, members, writer2); + writer1Dialect = await getDialectForMembers( + program, + members, + writer1.encryptionProps, + ); + writer2Dialect = await getDialectForMembers( + program, + members, + writer2.encryptionProps, + ); // then check writer1 dialect state const message1Writer1 = writer1Dialect.dialect.messages[1]; const message2Writer1 = writer1Dialect.dialect.messages[0]; chai.expect(message1Writer1.text).to.be.eq(writer1Text); - chai.expect(message1Writer1.owner).to.be.deep.eq(writer1.publicKey); + chai.expect(message1Writer1.owner).to.be.deep.eq(writer1.user.publicKey); chai.expect(message2Writer1.text).to.be.eq(writer2Text); - chai.expect(message2Writer1.owner).to.be.deep.eq(writer2.publicKey); + chai.expect(message2Writer1.owner).to.be.deep.eq(writer2.user.publicKey); // then check writer2 dialect state const message1Writer2 = writer2Dialect.dialect.messages[1]; const message2Writer2 = writer2Dialect.dialect.messages[0]; chai.expect(message1Writer2.text).to.be.eq(writer1Text); - chai.expect(message1Writer2.owner).to.be.deep.eq(writer1.publicKey); + chai.expect(message1Writer2.owner).to.be.deep.eq(writer1.user.publicKey); chai.expect(message2Writer2.text).to.be.eq(writer2Text); - chai.expect(message2Writer2.owner).to.be.deep.eq(writer2.publicKey); + chai.expect(message2Writer2.owner).to.be.deep.eq(writer2.user.publicKey); }); // This test was failing before changing nonce generation algorithm @@ -884,48 +962,68 @@ describe('Protocol v1 test', () => { }); members = [ { - publicKey: writer1.publicKey, + publicKey: writer1.user.publicKey, scopes: [true, true], // owner, read-only }, { - publicKey: writer2.publicKey, + publicKey: writer2.user.publicKey, scopes: [false, true], // non-owner, read-write }, ]; - await createDialect(program, writer1, members, true); + await createDialect(program, writer1.user, members, true); // when let writer1Dialect = await getDialectForMembers( program, members, - writer1, + writer1.encryptionProps, ); let writer2Dialect = await getDialectForMembers( program, members, - writer2, + writer2.encryptionProps, ); // ensures no dialect state linearization const writer1Text = generateRandomText(256); - await sendMessage(program, writer1Dialect, writer1, writer1Text); + await sendMessage( + program, + writer1Dialect, + writer1.user, + writer1Text, + writer1.encryptionProps, + ); const writer2Text = generateRandomText(256); - await sendMessage(program, writer2Dialect, writer2, writer2Text); + await sendMessage( + program, + writer2Dialect, + writer2.user, + writer2Text, + writer2.encryptionProps, + ); - writer1Dialect = await getDialectForMembers(program, members, writer1); - writer2Dialect = await getDialectForMembers(program, members, writer2); + writer1Dialect = await getDialectForMembers( + program, + members, + writer1.encryptionProps, + ); + writer2Dialect = await getDialectForMembers( + program, + members, + writer2.encryptionProps, + ); // then check writer1 dialect state const message1Writer1 = writer1Dialect.dialect.messages[1]; const message2Writer1 = writer1Dialect.dialect.messages[0]; chai.expect(message1Writer1.text).to.be.eq(writer1Text); - chai.expect(message1Writer1.owner).to.be.deep.eq(writer1.publicKey); + chai.expect(message1Writer1.owner).to.be.deep.eq(writer1.user.publicKey); chai.expect(message2Writer1.text).to.be.eq(writer2Text); - chai.expect(message2Writer1.owner).to.be.deep.eq(writer2.publicKey); + chai.expect(message2Writer1.owner).to.be.deep.eq(writer2.user.publicKey); // then check writer2 dialect state const message1Writer2 = writer2Dialect.dialect.messages[1]; const message2Writer2 = writer2Dialect.dialect.messages[0]; chai.expect(message1Writer2.text).to.be.eq(writer1Text); - chai.expect(message1Writer2.owner).to.be.deep.eq(writer1.publicKey); + chai.expect(message1Writer2.owner).to.be.deep.eq(writer1.user.publicKey); chai.expect(message2Writer2.text).to.be.eq(writer2Text); - chai.expect(message2Writer2.owner).to.be.deep.eq(writer2.publicKey); + chai.expect(message2Writer2.owner).to.be.deep.eq(writer2.user.publicKey); }); }); @@ -937,11 +1035,11 @@ describe('Protocol v1 test', () => { owner = await createUser({ requestAirdrop: true, createMeta: false, - }); + }).then((it) => it.user); writer = await createUser({ requestAirdrop: true, createMeta: false, - }); + }).then((it) => it.user); }); it('Can subscribe to events and receive them and unsubscribe', async () => { @@ -993,7 +1091,14 @@ describe('Protocol v1 test', () => { if (createMeta) { await createMetadata(program, user); } - return user; + const encryptionProps = { + ed25519PublicKey: user.publicKey.toBytes(), + diffieHellmanKeyPair: ed25519KeyPairToCurve25519({ + publicKey: user.publicKey.toBytes(), + secretKey: user.secretKey, + }), + }; + return { user, encryptionProps }; } });