-
Notifications
You must be signed in to change notification settings - Fork 46
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #177 from xmtp/negotiated-topic-invitation-class
Negotiated topic invitation classes
- Loading branch information
Showing
9 changed files
with
436 additions
and
11 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
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,202 @@ | ||
import Long from 'long' | ||
import { SignedPublicKeyBundle } from './crypto/PublicKeyBundle' | ||
import { invitation } from '@xmtp/proto' | ||
import Ciphertext from './crypto/Ciphertext' | ||
import { decrypt, encrypt } from './crypto' | ||
import { PrivateKeyBundleV2 } from './crypto/PrivateKeyBundle' | ||
import { dateToNs } from './utils' | ||
|
||
/** | ||
* InvitationV1 is a protobuf message to be encrypted and used as the ciphertext in a SealedInvitationV1 message | ||
*/ | ||
export class InvitationV1 implements invitation.InvitationV1 { | ||
topic: string | ||
aes256GcmHkdfSha256: invitation.InvitationV1_Aes256gcmHkdfsha256 // eslint-disable-line camelcase | ||
|
||
constructor({ topic, aes256GcmHkdfSha256 }: invitation.InvitationV1) { | ||
if (!topic || !topic.length) { | ||
throw new Error('Missing topic') | ||
} | ||
if ( | ||
!aes256GcmHkdfSha256 || | ||
!aes256GcmHkdfSha256.keyMaterial || | ||
!aes256GcmHkdfSha256.keyMaterial.length | ||
) { | ||
throw new Error('Missing key material') | ||
} | ||
this.topic = topic | ||
this.aes256GcmHkdfSha256 = aes256GcmHkdfSha256 | ||
} | ||
|
||
toBytes(): Uint8Array { | ||
return invitation.InvitationV1.encode(this).finish() | ||
} | ||
|
||
static fromBytes(bytes: Uint8Array): InvitationV1 { | ||
return new InvitationV1(invitation.InvitationV1.decode(bytes)) | ||
} | ||
} | ||
|
||
/** | ||
* SealedInvitationHeaderV1 is a protobuf message to be used as the headerBytes in a SealedInvitationV1 | ||
*/ | ||
export class SealedInvitationHeaderV1 | ||
implements invitation.SealedInvitationHeaderV1 | ||
{ | ||
sender: SignedPublicKeyBundle | ||
recipient: SignedPublicKeyBundle | ||
createdNs: Long | ||
|
||
constructor({ | ||
sender, | ||
recipient, | ||
createdNs, | ||
}: invitation.SealedInvitationHeaderV1) { | ||
if (!sender) { | ||
throw new Error('Missing sender') | ||
} | ||
if (!recipient) { | ||
throw new Error('Missing recipient') | ||
} | ||
this.sender = new SignedPublicKeyBundle(sender) | ||
this.recipient = new SignedPublicKeyBundle(recipient) | ||
this.createdNs = createdNs | ||
} | ||
|
||
toBytes(): Uint8Array { | ||
return invitation.SealedInvitationHeaderV1.encode(this).finish() | ||
} | ||
|
||
static fromBytes(bytes: Uint8Array): SealedInvitationHeaderV1 { | ||
return new SealedInvitationHeaderV1( | ||
invitation.SealedInvitationHeaderV1.decode(bytes) | ||
) | ||
} | ||
} | ||
|
||
export class SealedInvitationV1 implements invitation.SealedInvitationV1 { | ||
headerBytes: Uint8Array | ||
ciphertext: Ciphertext | ||
private _header?: SealedInvitationHeaderV1 | ||
private _invitation?: InvitationV1 | ||
|
||
constructor({ headerBytes, ciphertext }: invitation.SealedInvitationV1) { | ||
if (!headerBytes || !headerBytes.length) { | ||
throw new Error('Missing header bytes') | ||
} | ||
if (!ciphertext) { | ||
throw new Error('Missing ciphertext') | ||
} | ||
this.headerBytes = headerBytes | ||
this.ciphertext = new Ciphertext(ciphertext) | ||
} | ||
|
||
/** | ||
* Accessor method for the full header object | ||
*/ | ||
get header(): SealedInvitationHeaderV1 { | ||
// Use cached value if already exists | ||
if (this._header) { | ||
return this._header | ||
} | ||
this._header = SealedInvitationHeaderV1.fromBytes(this.headerBytes) | ||
return this._header | ||
} | ||
|
||
/** | ||
* getInvitation decrypts and returns the InvitationV1 stored in the ciphertext of the Sealed Invitation | ||
*/ | ||
async getInvitation(viewer: PrivateKeyBundleV2): Promise<InvitationV1> { | ||
// Use cached value if already exists | ||
if (this._invitation) { | ||
return this._invitation | ||
} | ||
// The constructors for child classes will validate that this is complete | ||
const header = this.header | ||
let secret: Uint8Array | ||
if (viewer.identityKey.matches(this.header.sender.identityKey)) { | ||
secret = await viewer.sharedSecret( | ||
header.recipient, | ||
header.sender.preKey, | ||
false | ||
) | ||
} else { | ||
secret = await viewer.sharedSecret( | ||
header.sender, | ||
header.recipient.preKey, | ||
true | ||
) | ||
} | ||
|
||
const decryptedBytes = await decrypt( | ||
this.ciphertext, | ||
secret, | ||
this.headerBytes | ||
) | ||
this._invitation = InvitationV1.fromBytes(decryptedBytes) | ||
return this._invitation | ||
} | ||
|
||
toBytes(): Uint8Array { | ||
return invitation.SealedInvitationV1.encode(this).finish() | ||
} | ||
|
||
static fromBytes(bytes: Uint8Array): SealedInvitationV1 { | ||
return new SealedInvitationV1(invitation.SealedInvitationV1.decode(bytes)) | ||
} | ||
} | ||
|
||
/** | ||
* Wrapper class for SealedInvitationV1 and any future iterations of SealedInvitation | ||
*/ | ||
export class SealedInvitation implements invitation.SealedInvitation { | ||
v1: SealedInvitationV1 | ||
|
||
constructor({ v1 }: invitation.SealedInvitation) { | ||
if (!v1) { | ||
throw new Error('Missing v1 invitation') | ||
} | ||
this.v1 = new SealedInvitationV1(v1) | ||
} | ||
|
||
toBytes(): Uint8Array { | ||
return invitation.SealedInvitation.encode(this).finish() | ||
} | ||
|
||
static fromBytes(bytes: Uint8Array): SealedInvitation { | ||
return new SealedInvitation(invitation.SealedInvitation.decode(bytes)) | ||
} | ||
|
||
/** | ||
* Create a SealedInvitation with a SealedInvitationV1 payload | ||
* Will encrypt all contents and validate inputs | ||
*/ | ||
static async createV1({ | ||
sender, | ||
recipient, | ||
created, | ||
invitation, | ||
}: { | ||
sender: PrivateKeyBundleV2 | ||
recipient: SignedPublicKeyBundle | ||
created: Date | ||
invitation: InvitationV1 | ||
}): Promise<SealedInvitation> { | ||
const headerBytes = new SealedInvitationHeaderV1({ | ||
sender: sender.getPublicKeyBundle(), | ||
recipient, | ||
createdNs: dateToNs(created), | ||
}).toBytes() | ||
|
||
const secret = await sender.sharedSecret( | ||
recipient, | ||
sender.getCurrentPreKey().publicKey, | ||
false | ||
) | ||
|
||
const invitationBytes = invitation.toBytes() | ||
const ciphertext = await encrypt(invitationBytes, secret, headerBytes) | ||
|
||
return new SealedInvitation({ v1: { headerBytes, ciphertext } }) | ||
} | ||
} |
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
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
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
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
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
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
Oops, something went wrong.