// Copyright 2016 Keybase, Inc. All rights reserved. Use of // this source code is governed by the included BSD license. package chat import ( "bytes" "crypto/hmac" "crypto/rand" "crypto/sha256" "encoding/binary" "encoding/hex" "errors" "flag" "fmt" "sort" "sync" "time" "golang.org/x/crypto/nacl/box" "golang.org/x/crypto/nacl/secretbox" "golang.org/x/net/context" "golang.org/x/sync/errgroup" "github.com/keybase/client/go/chat/globals" "github.com/keybase/client/go/chat/signencrypt" "github.com/keybase/client/go/chat/storage" "github.com/keybase/client/go/chat/types" "github.com/keybase/client/go/chat/utils" "github.com/keybase/client/go/ephemeral" "github.com/keybase/client/go/kbcrypto" "github.com/keybase/client/go/libkb" "github.com/keybase/client/go/logger" "github.com/keybase/client/go/protocol/chat1" "github.com/keybase/client/go/protocol/gregor1" "github.com/keybase/client/go/protocol/keybase1" "github.com/keybase/client/go/teambot" "github.com/keybase/client/go/teams" "github.com/keybase/clockwork" "github.com/keybase/go-codec/codec" "github.com/keybase/go-crypto/ed25519" ) const CurrentMessageBoxedVersion = chat1.MessageBoxedVersion_V2 var publicCryptKey keybase1.CryptKey func init() { // publicCryptKey is a zero key used for public chat messages. var zero [libkb.NaclDHKeySecretSize]byte publicCryptKey = keybase1.CryptKey{ KeyGeneration: 1, Key: keybase1.Bytes32(zero), } } type Boxer struct { utils.DebugLabeler globals.Contextified boxVersionForTesting *chat1.MessageBoxedVersion // Replaceable for testing. // Normally set to normal implementations. hashV1 func(data []byte) chat1.Hash // Slots for replacing with test implementations. // These are normally nil. testingValidSenderKey func(context.Context, gregor1.UID, []byte, gregor1.Time) (revoked *gregor1.Time, unboxingErr types.UnboxingError) testingGetSenderInfoLocal func(context.Context, gregor1.UID, gregor1.DeviceID) (senderUsername string, senderDeviceName string, senderDeviceType keybase1.DeviceTypeV2) // Post-process signatures and signencrypts testingSignatureMangle func([]byte) []byte clock clockwork.Clock } func NewBoxer(g *globals.Context) *Boxer { return &Boxer{ DebugLabeler: utils.NewDebugLabeler(g.ExternalG(), "Boxer", false), hashV1: hashSha256V1, Contextified: globals.NewContextified(g), clock: clockwork.NewRealClock(), } } func (b *Boxer) SetClock(clock clockwork.Clock) { b.clock = clock } func (b *Boxer) log() logger.Logger { return b.G().GetLog() } func (b *Boxer) makeErrorMessageFromPieces(ctx context.Context, err types.UnboxingError, msgID chat1.MessageID, msgType chat1.MessageType, ctime gregor1.Time, sender gregor1.UID, senderDevice gregor1.DeviceID, botUID *gregor1.UID, isEphemeral bool, explodedBy *string, etime gregor1.Time) chat1.MessageUnboxed { e := chat1.MessageUnboxedError{ ErrType: err.ExportType(), ErrMsg: err.Error(), InternalErrMsg: err.InternalError(), VersionKind: err.VersionKind(), VersionNumber: err.VersionNumber(), IsCritical: err.IsCritical(), MessageID: msgID, MessageType: msgType, Ctime: ctime, IsEphemeral: isEphemeral, ExplodedBy: explodedBy, Etime: etime, BotUsername: b.getBotInfoLocal(ctx, botUID), } e.SenderUsername, e.SenderDeviceName, e.SenderDeviceType = b.getSenderInfoLocal(ctx, sender, senderDevice) return chat1.NewMessageUnboxedWithError(e) } func (b *Boxer) makeErrorMessage(ctx context.Context, msg chat1.MessageBoxed, err types.UnboxingError) chat1.MessageUnboxed { return b.makeErrorMessageFromPieces(ctx, err, msg.GetMessageID(), msg.GetMessageType(), msg.ServerHeader.Ctime, msg.ClientHeader.Sender, msg.ClientHeader.SenderDevice, msg.ClientHeader.BotUID, msg.IsEphemeral(), msg.ExplodedBy(), msg.Etime()) } func (b *Boxer) detectPermanentError(conv types.UnboxConversationInfo, err error, tlfName string) types.UnboxingError { // Check for team not exist error that is in raw form if aerr, ok := err.(libkb.AppStatusError); ok { switch keybase1.StatusCode(aerr.Code) { case keybase1.StatusCode_SCTeamNotFound: return NewPermanentUnboxingError(err) default: // Nothing to do. } } if _, ok := IsRekeyError(err); ok && conv.GetFinalizeInfo() != nil { return NewPermanentUnboxingError(err) } // Check if we have a permanent or tranisent team read error. Transient // errors, are converted to rekey errors later. if teams.IsTeamReadError(err) { switch err.Error() { case "Root team has been deleted", "Root team has been abandoned: All members have left or reset", "Root team is not active": return NewPermanentUnboxingError(err) default: return NewTransientUnboxingError(err) } } switch err := err.(type) { case libkb.UserDeletedError: if len(err.Msg) == 0 { err.Msg = fmt.Sprintf("user deleted in chat '%v'", tlfName) } return NewPermanentUnboxingError(err) case teams.TeamDoesNotExistError, teams.KBFSKeyGenerationError, libkb.KeyMaskNotFoundError, libkb.AssertionCheckError, DecryptionKeyNotFoundError, NotAuthenticatedForThisDeviceError, InvalidMACError, ImpteamBadteamError, teambot.TeambotPermanentKeyError: return NewPermanentUnboxingError(err) case ephemeral.EphemeralKeyError: // Normalize error message with EphemeralUnboxingError ekErr := NewEphemeralUnboxingError(err) if err.IsPermanent() { return NewPermanentUnboxingError(ekErr) } return NewTransientUnboxingError(ekErr) } // Check for no space left on device errors if libkb.IsNoSpaceOnDeviceError(err) { return NewPermanentUnboxingError(err) } // transient error. Rekey errors come through here return NewTransientUnboxingError(err) } type basicUnboxConversationInfo struct { convID chat1.ConversationID membersType chat1.ConversationMembersType finalizeInfo *chat1.ConversationFinalizeInfo visibility keybase1.TLFVisibility } var _ types.UnboxConversationInfo = (*basicUnboxConversationInfo)(nil) func newBasicUnboxConversationInfo(convID chat1.ConversationID, membersType chat1.ConversationMembersType, finalizeInfo *chat1.ConversationFinalizeInfo, visibility keybase1.TLFVisibility) *basicUnboxConversationInfo { return &basicUnboxConversationInfo{ convID: convID, membersType: membersType, finalizeInfo: finalizeInfo, visibility: visibility, } } func (b *basicUnboxConversationInfo) GetConvID() chat1.ConversationID { return b.convID } func (b *basicUnboxConversationInfo) GetMembersType() chat1.ConversationMembersType { return b.membersType } func (b *basicUnboxConversationInfo) GetFinalizeInfo() *chat1.ConversationFinalizeInfo { return b.finalizeInfo } func (b *basicUnboxConversationInfo) GetExpunge() *chat1.Expunge { return nil } func (b *basicUnboxConversationInfo) GetMaxDeletedUpTo() chat1.MessageID { return 0 } func (b *basicUnboxConversationInfo) IsPublic() bool { return b.visibility == keybase1.TLFVisibility_PUBLIC } func (b *basicUnboxConversationInfo) GetMaxMessage(chat1.MessageType) (chat1.MessageSummary, error) { return chat1.MessageSummary{}, nil } type extraInboxUnboxConversationInfo struct { convID chat1.ConversationID membersType chat1.ConversationMembersType visibility keybase1.TLFVisibility } var _ types.UnboxConversationInfo = (*extraInboxUnboxConversationInfo)(nil) func newExtraInboxUnboxConverstionInfo(convID chat1.ConversationID, membersType chat1.ConversationMembersType, visibility keybase1.TLFVisibility) *extraInboxUnboxConversationInfo { return &extraInboxUnboxConversationInfo{ convID: convID, membersType: membersType, visibility: visibility, } } func (p *extraInboxUnboxConversationInfo) GetConvID() chat1.ConversationID { return p.convID } func (p *extraInboxUnboxConversationInfo) GetMembersType() chat1.ConversationMembersType { return p.membersType } func (p *extraInboxUnboxConversationInfo) GetFinalizeInfo() *chat1.ConversationFinalizeInfo { return nil } func (p *extraInboxUnboxConversationInfo) GetExpunge() *chat1.Expunge { return nil } func (p *extraInboxUnboxConversationInfo) GetMaxDeletedUpTo() chat1.MessageID { return 0 } func (p *extraInboxUnboxConversationInfo) IsPublic() bool { return p.visibility == keybase1.TLFVisibility_PUBLIC } func (p *extraInboxUnboxConversationInfo) GetMaxMessage(chat1.MessageType) (chat1.MessageSummary, error) { return chat1.MessageSummary{}, nil } func (b *Boxer) getEffectiveMembersType(ctx context.Context, boxed chat1.MessageBoxed, convMembersType chat1.ConversationMembersType) chat1.ConversationMembersType { switch convMembersType { case chat1.ConversationMembersType_IMPTEAMUPGRADE: if boxed.KBFSEncrypted() { b.Debug(ctx, "getEffectiveMembersType: overruling %v conv with KBFS keys", convMembersType) return chat1.ConversationMembersType_KBFS } default: // Nothing to do for other conv types. } return convMembersType } var errBoxerUnavailableMessage = NewPermanentUnboxingError(errors.New("message not available")) func (b *Boxer) castInternalError(ierr types.UnboxingError) error { err, ok := ierr.(error) if ok { return err } return nil } // UnboxMessage unboxes a chat1.MessageBoxed into a chat1.MessageUnboxed. It // finds the appropriate keybase1.CryptKey, decrypts the message, and verifies // several things: // - The message's signature is valid. // - (TODO) The signing KID was valid when the signature was made. // - (TODO) The signing KID belongs to the sending device. // - (TODO) The sending device belongs to the sender. // [Note that we do currently check the KID -> UID relationship, // independent of the device ID.] // - (TODO) The sender has write permission in the TLF. // - (TODO) The TLF name, public flag, and finalized info resolve to the TLF ID. // - The conversation ID derives from the ConversationIDTriple. // - The body hash is not a replay from another message we know about. // - The prev pointers are consistent with other messages we know about. // - (TODO) The prev pointers are not absurdly ancient. // - The ClientHeader provided with the BoxedMessage matches the one we decrypt. // // The first return value is unusable if the err != nil. Returns (_, err) for // non-permanent errors, and (MessageUnboxedError, nil) for permanent errors. // Permanent errors can be cached and must be treated as a value to deal with, // whereas temporary errors are transient failures. func (b *Boxer) UnboxMessage(ctx context.Context, boxed chat1.MessageBoxed, conv types.UnboxConversationInfo, info *types.BoxerEncryptionInfo) (m chat1.MessageUnboxed, uberr types.UnboxingError) { ctx = libkb.WithLogTag(ctx, "CHTUNBOX") var err error defer b.Trace(ctx, &err, "UnboxMessage(%s, %d)", conv.GetConvID(), boxed.GetMessageID())() defer func() { err = b.castInternalError(uberr) }() // Check to see if the context has been cancelled select { case <-ctx.Done(): return m, NewTransientUnboxingError(ctx.Err()) default: } // if the server message doesn't have a TLFID, then it isn't available to us. This is most commonly // used when restricted bots try to read messages not encrypted for them. if boxed.ClientHeader.Conv.Tlfid.IsNil() { return b.makeErrorMessage(ctx, boxed, errBoxerUnavailableMessage), nil } // If we don't have an rtime, add one. if boxed.ServerHeader.Rtime == nil { now := gregor1.ToTime(b.clock.Now()) boxed.ServerHeader.Rtime = &now } tlfName := boxed.ClientHeader.TLFNameExpanded(conv.GetFinalizeInfo()) if conv.IsPublic() != boxed.ClientHeader.TlfPublic { return b.makeErrorMessage(ctx, boxed, NewPermanentUnboxingError(fmt.Errorf("visibility mismatch: %v != %v", conv.IsPublic(), boxed.ClientHeader.TlfPublic))), nil } if info == nil { info = new(types.BoxerEncryptionInfo) keyMembersType := b.getEffectiveMembersType(ctx, boxed, conv.GetMembersType()) encryptionKey, err := globals.CtxKeyFinder(ctx, b.G()).FindForDecryption(ctx, tlfName, boxed.ClientHeader.Conv.Tlfid, conv.GetMembersType(), conv.IsPublic(), boxed.KeyGeneration, keyMembersType == chat1.ConversationMembersType_KBFS, boxed.ClientHeader.BotUID) if err != nil { // Post-process error from this uberr = b.detectPermanentError(conv, err, tlfName) if uberr.IsPermanent() { return b.makeErrorMessage(ctx, boxed, uberr), nil } return chat1.MessageUnboxed{}, uberr } // If the message is exploding, load the ephemeral key. var ephemeralKey types.EphemeralCryptKey if boxed.IsEphemeral() { ephemeralKey, err = globals.CtxKeyFinder(ctx, b.G()).EphemeralKeyForDecryption( b.G().MetaContext(ctx), tlfName, boxed.ClientHeader.Conv.Tlfid, conv.GetMembersType(), boxed.ClientHeader.TlfPublic, boxed.ClientHeader.BotUID, boxed.EphemeralMetadata().Generation, &boxed.ServerHeader.Ctime) if err != nil { b.Debug(ctx, "failed to get a key for ephemeral message: msgID: %d err: %v", boxed.ServerHeader.MessageID, err) uberr = b.detectPermanentError(conv, err, tlfName) if uberr.IsPermanent() { return b.makeErrorMessage(ctx, boxed, uberr), nil } return chat1.MessageUnboxed{}, uberr } } info.Key = encryptionKey info.EphemeralKey = ephemeralKey } unboxed, ierr := b.unbox(ctx, boxed, conv, info.Key, info.EphemeralKey) if ierr == nil { ierr = b.checkInvariants(ctx, conv.GetConvID(), boxed, unboxed) } if ierr != nil { b.Debug(ctx, "failed to unbox message: msgID: %d err: %s", boxed.ServerHeader.MessageID, ierr.Error()) if ierr.IsPermanent() { return b.makeErrorMessage(ctx, boxed, ierr), nil } return chat1.MessageUnboxed{}, ierr } return chat1.NewMessageUnboxedWithValid(*unboxed), nil } func (b *Boxer) checkInvariants(ctx context.Context, convID chat1.ConversationID, boxed chat1.MessageBoxed, unboxed *chat1.MessageUnboxedValid) types.UnboxingError { // Check that the ConversationIDTriple in the signed message header matches // the conversation ID we were expecting. if !unboxed.ClientHeader.Conv.Derivable(convID) { err := fmt.Errorf("conversation ID mismatch: header: %x convID: %s", unboxed.ClientHeader.Conv.Hash(), convID) return NewPermanentUnboxingError(err) } // Check that message type on the client header matches the body body := unboxed.MessageBody if !body.IsNil() { bodyTyp, err := body.MessageType() if err != nil { return NewPermanentUnboxingError(err) } if unboxed.ClientHeader.MessageType != bodyTyp { err := fmt.Errorf("client header message type does not match body: %v(header) != %v(body)", unboxed.ClientHeader.MessageType, bodyTyp) return NewPermanentUnboxingError(err) } } // Make sure the body hash is unique to this message, and then record it. // This detects attempts by the server to replay a message. Right now we // use a "first writer wins" rule here, though we could also consider a // "duplication invalidates both" rule if we wanted to be stricter. Note // that this only prevents replays of messages you *know about*. There's // currently nothing stopping the server from replaying an ancient message // if you're never going to fetch enough history to notice. // // But...wait...why aren't we using the header hash here? It covers more // stuff, and we're using it below, when we check the consistency of // messages and prev pointers... // // The reason we can't use the header hash to prevent replays is that it's // a hash *of* a signature, rather than a hash that's *been signed*. // Unfortunately, most signature schemes are "malleable" in some way, // depending on the implementation. See // http://crypto.stackexchange.com/a/14719/21442. If I have the shared // encryption key (and recall that in public chats, everyone does), I can // decrypt the message, twiddle your signature into another valid signature // over the same plaintext, reencrypt the whole thing, and pass it off as a // new valid message with a seemingly new signature and therefore a unique // header hash. Because the body hash is unique to each message (derived // from a random nonce), and because it's *inside* the signature, we use // that to detect replays instead. replayErr := storage.CheckAndRecordBodyHash(ctx, b.G(), unboxed.BodyHash, boxed.ServerHeader.MessageID, convID) if replayErr != nil { b.Debug(ctx, "UnboxMessage found a replayed body hash: %s", replayErr) return NewPermanentUnboxingError(replayErr) } // Make sure the header hash and prev pointers of this message are // consistent with every other message we've seen, and record them. // // The discussion above explains why we have to use the body hash there to // prevent replays. But we need to use the header hash here, because it's // the only thing that covers the entire message. The goal isn't to prevent // the creation of new messages (as it was above), but to prevent an old // message from changing. prevPtrErr := storage.CheckAndRecordPrevPointer(ctx, b.G(), boxed.ServerHeader.MessageID, convID, unboxed.HeaderHash) if prevPtrErr != nil { b.Debug(ctx, "UnboxMessage found an inconsistent header hash: %s", prevPtrErr) return NewPermanentUnboxingError(prevPtrErr) } for _, prevPtr := range unboxed.ClientHeader.Prev { prevPtrErr := storage.CheckAndRecordPrevPointer(ctx, b.G(), prevPtr.Id, convID, prevPtr.Hash) if prevPtrErr != nil { b.Debug(ctx, "UnboxMessage found an inconsistent prev pointer: %s", prevPtrErr) return NewPermanentUnboxingError(prevPtrErr) } } return nil } func (b *Boxer) unbox(ctx context.Context, boxed chat1.MessageBoxed, conv types.UnboxConversationInfo, encryptionKey types.CryptKey, ephemeralKey types.EphemeralCryptKey) (*chat1.MessageUnboxedValid, types.UnboxingError) { switch boxed.Version { case chat1.MessageBoxedVersion_VNONE, chat1.MessageBoxedVersion_V1: res, err := b.unboxV1(ctx, boxed, conv, encryptionKey) if err != nil { b.Debug(ctx, "error unboxing message version: %v", boxed.Version) } return res, err // V3 is the same as V2, except that it indicates exploding message support. // V4 is the same as V3, except if pairwise MACs are included, then the sender signing key is a dummy. case chat1.MessageBoxedVersion_V2, chat1.MessageBoxedVersion_V3, chat1.MessageBoxedVersion_V4: res, err := b.unboxV2orV3orV4(ctx, boxed, conv, encryptionKey, ephemeralKey) if err != nil { b.Debug(ctx, "error unboxing message version: %v, %s", boxed.Version, err) } return res, err // NOTE: When adding new versions here, you must also update // chat1/extras.go so MessageUnboxedError.ParseableVersion understands the // new max version default: return nil, NewPermanentUnboxingError(NewMessageBoxedVersionError(boxed.Version)) } } func (b *Boxer) headerUnsupported(ctx context.Context, headerVersion chat1.HeaderPlaintextVersion, header chat1.HeaderPlaintext) chat1.HeaderPlaintextUnsupported { switch headerVersion { case chat1.HeaderPlaintextVersion_V2: return header.V2() case chat1.HeaderPlaintextVersion_V3: return header.V3() case chat1.HeaderPlaintextVersion_V4: return header.V4() case chat1.HeaderPlaintextVersion_V5: return header.V5() case chat1.HeaderPlaintextVersion_V6: return header.V6() case chat1.HeaderPlaintextVersion_V7: return header.V7() case chat1.HeaderPlaintextVersion_V8: return header.V8() case chat1.HeaderPlaintextVersion_V9: return header.V9() case chat1.HeaderPlaintextVersion_V10: return header.V10() default: b.Debug(ctx, "headerUnsupported: unknown version: %v", headerVersion) return chat1.HeaderPlaintextUnsupported{ Mi: chat1.HeaderPlaintextMetaInfo{ Crit: true, }, } } } func (b *Boxer) bodyUnsupported(ctx context.Context, bodyVersion chat1.BodyPlaintextVersion, body chat1.BodyPlaintext) chat1.BodyPlaintextUnsupported { switch bodyVersion { case chat1.BodyPlaintextVersion_V3: return body.V3() case chat1.BodyPlaintextVersion_V4: return body.V4() case chat1.BodyPlaintextVersion_V5: return body.V5() case chat1.BodyPlaintextVersion_V6: return body.V6() case chat1.BodyPlaintextVersion_V7: return body.V7() case chat1.BodyPlaintextVersion_V8: return body.V8() case chat1.BodyPlaintextVersion_V9: return body.V9() case chat1.BodyPlaintextVersion_V10: return body.V10() default: b.Debug(ctx, "bodyUnsupported: unknown version: %v", bodyVersion) return chat1.BodyPlaintextUnsupported{ Mi: chat1.BodyPlaintextMetaInfo{ Crit: true, }, } } } // unboxV1 unboxes a chat1.MessageBoxed into a keybase1.Message given // a keybase1.CryptKey. func (b *Boxer) unboxV1(ctx context.Context, boxed chat1.MessageBoxed, conv types.UnboxConversationInfo, encryptionKey types.CryptKey) (*chat1.MessageUnboxedValid, types.UnboxingError) { var err error if boxed.ServerHeader == nil { return nil, NewPermanentUnboxingError(errors.New("nil ServerHeader in MessageBoxed")) } if len(boxed.VerifyKey) != 0 { return nil, NewPermanentUnboxingError(errors.New("populated VerifyKey in MBV1")) } // compute the header hash headerHash := b.hashV1(boxed.HeaderCiphertext.E) // Whether the body is missing (deleted) skipBodyVerification := (len(boxed.BodyCiphertext.E) == 0) // TODO We should check whether the body is allowed to have been deleted by checking // that there is in fact a message that deleted it. // We should fetch that message and check its signed body. // That involves fetching a message whose ID is not known here. // decrypt body // will remain empty if the body was deleted var bodyVersioned chat1.BodyPlaintext if !skipBodyVerification { packedBody, err := b.open(boxed.BodyCiphertext, libkb.NaclSecretBoxKey(encryptionKey.Material())) if err != nil { return nil, NewPermanentUnboxingError(err) } if err := b.unmarshal(packedBody, &bodyVersioned); err != nil { return nil, NewPermanentUnboxingError(err) } } // decrypt header packedHeader, err := b.open(boxed.HeaderCiphertext.AsEncrypted(), libkb.NaclSecretBoxKey(encryptionKey.Material())) if err != nil { return nil, NewPermanentUnboxingError(err) } var header chat1.HeaderPlaintext if err := b.unmarshal(packedHeader, &header); err != nil { return nil, NewPermanentUnboxingError(err) } // verify the message validity, ierr := b.verifyMessageV1(ctx, header, boxed, skipBodyVerification) if ierr != nil { return nil, ierr } // create a chat1.MessageClientHeader from versioned HeaderPlaintext var clientHeader chat1.MessageClientHeaderVerified headerVersion, err := header.Version() if err != nil { return nil, NewPermanentUnboxingError(err) } rtime := gregor1.ToTime(b.clock.Now()) if boxed.ServerHeader.Rtime != nil { rtime = *boxed.ServerHeader.Rtime } var headerSignature *chat1.SignatureInfo var bodyHash chat1.Hash switch headerVersion { case chat1.HeaderPlaintextVersion_V1: // Verified above in verifyMessageV1 headerSignature = header.V1().HeaderSignature hp := header.V1() bodyHash = hp.BodyHash clientHeader = chat1.MessageClientHeaderVerified{ Conv: hp.Conv, TlfName: hp.TlfName, TlfPublic: hp.TlfPublic, MessageType: hp.MessageType, Prev: hp.Prev, Sender: hp.Sender, SenderDevice: hp.SenderDevice, // MerkleRoot is not expected to be in any v1 messages. Ignore it. MerkleRoot: nil, OutboxID: hp.OutboxID, OutboxInfo: hp.OutboxInfo, KbfsCryptKeysUsed: hp.KbfsCryptKeysUsed, Rtime: rtime, } // NOTE: When adding new versions here, you must also update // chat1/extras.go so MessageUnboxedError.ParseableVersion understands the // new max version default: return nil, NewPermanentUnboxingError(NewHeaderVersionError(headerVersion, b.headerUnsupported(ctx, headerVersion, header))) } // Check for sender match on the inner and outer header. if !clientHeader.Sender.Eq(boxed.ClientHeader.Sender) { return nil, NewPermanentUnboxingError(fmt.Errorf("sender does not match")) } if !bytes.Equal(clientHeader.SenderDevice.Bytes(), boxed.ClientHeader.SenderDevice.Bytes()) { return nil, NewPermanentUnboxingError(fmt.Errorf("sender device does not match")) } // Any of (senderUsername, senderDeviceName, senderDeviceType) could be empty strings because of non-critical failures. senderUsername, senderDeviceName, senderDeviceType := b.getSenderInfoLocal( ctx, clientHeader.Sender, clientHeader.SenderDevice) // create a chat1.MessageBody from versioned chat1.BodyPlaintext // Will remain empty if the body was deleted. var body chat1.MessageBody if !skipBodyVerification { bodyVersion, err := bodyVersioned.Version() if err != nil { return nil, NewPermanentUnboxingError(err) } switch bodyVersion { case chat1.BodyPlaintextVersion_V1: body = bodyVersioned.V1().MessageBody // NOTE: When adding new versions here, you must also update // chat1/extras.go so MessageUnboxedError.ParseableVersion understands the // new max version default: return nil, NewPermanentUnboxingError(NewBodyVersionError(bodyVersion, b.bodyUnsupported(ctx, bodyVersion, bodyVersioned))) } } // Get at mention usernames atMentions, atMentionUsernames, maybeRes, chanMention, channelNameMentions := b.getAtMentionInfo(ctx, clientHeader.Conv.Tlfid, clientHeader.Conv.TopicType, conv, body) ierr = b.compareHeadersMBV1(ctx, boxed.ClientHeader, clientHeader) if ierr != nil { return nil, ierr } // create an unboxed message return &chat1.MessageUnboxedValid{ ClientHeader: clientHeader, ServerHeader: *boxed.ServerHeader, MessageBody: body, SenderUsername: senderUsername, SenderDeviceName: senderDeviceName, SenderDeviceType: senderDeviceType, BodyHash: bodyHash, HeaderHash: headerHash, HeaderSignature: headerSignature, VerificationKey: &validity.validationKey, SenderDeviceRevokedAt: validity.senderDeviceRevokedAt, AtMentions: atMentions, AtMentionUsernames: atMentionUsernames, ChannelMention: chanMention, ChannelNameMentions: channelNameMentions, MaybeMentions: maybeRes, BotUsername: b.getBotInfoLocal(ctx, clientHeader.BotUID), Emojis: b.getEmojis(ctx, clientHeader.Conv.TopicType, body), }, nil } func (b *Boxer) memberCtime(mctx libkb.MetaContext, conv types.UnboxConversationInfo, tlfID chat1.TLFID, tlfName string) (*keybase1.Time, error) { team, err := NewTeamLoader(b.G().ExternalG()).loadTeam(mctx.Ctx(), tlfID, tlfName, conv.GetMembersType(), conv.IsPublic(), nil) if err != nil { return nil, err } uv, err := mctx.G().GetMeUV(mctx.Ctx()) if err != nil { return nil, err } return team.MemberCtime(mctx.Ctx(), uv), nil } func (b *Boxer) validatePairwiseMAC(ctx context.Context, boxed chat1.MessageBoxed, conv types.UnboxConversationInfo, headerHash chat1.Hash) (senderKey []byte, err error) { defer b.Trace(ctx, &err, "validatePairwiseMAC")() // First, find a MAC that matches our receiving device encryption KID. ourDeviceKeyNacl, err := b.G().ActiveDevice.NaclEncryptionKey() if err != nil { return nil, err } messageMAC, found := boxed.ClientHeader.PairwiseMacs[ourDeviceKeyNacl.GetKID()] if !found { // This is an error users will actually see when they've just joined a // team or added a new device. mctx := b.G().MetaContext(ctx) memberCtime, err := b.memberCtime(mctx, conv, boxed.ClientHeader.Conv.Tlfid, boxed.ClientHeader.TlfName) if err != nil { b.Debug(ctx, "Unable to get member ctime: %v", err) } return nil, NewNotAuthenticatedForThisDeviceError(mctx, memberCtime, boxed.ServerHeader.Ctime) } // Second, load the device encryption KID for the sender. senderUID, err := keybase1.UIDFromSlice(boxed.ClientHeader.Sender) if err != nil { return nil, err } senderDeviceID, err := keybase1.DeviceIDFromSlice(boxed.ClientHeader.SenderDevice) if err != nil { return nil, err } // Use the loading function that hits the server if-and-only-if we don't // have the given deviceID in cache. senderUPAK, err := b.G().GetUPAKLoader().LoadUPAKWithDeviceID(ctx, senderUID, senderDeviceID) if err != nil { return nil, err } senderEncryptionKID := senderUPAK.Current.FindEncryptionKIDFromDeviceID(senderDeviceID) if senderEncryptionKID.IsNil() { for _, upk := range senderUPAK.PastIncarnations { senderEncryptionKID = upk.FindEncryptionKIDFromDeviceID(senderDeviceID) if !senderEncryptionKID.IsNil() { break } } if senderEncryptionKID.IsNil() { return nil, fmt.Errorf("failed to find encryption key for device %s", senderDeviceID.String()) } } senderDeviceDHKeyNacl, err := libkb.ImportDHKeypairFromKID(senderEncryptionKID) if err != nil { return nil, err } // Finally, validate the MAC. computedMAC := makeOnePairwiseMAC(*ourDeviceKeyNacl.Private, senderDeviceDHKeyNacl.Public, headerHash) if !hmac.Equal(messageMAC, computedMAC) { return nil, NewInvalidMACError() } return senderEncryptionKID.ToBytes(), nil } func (b *Boxer) ResolveSkippedUnboxed(ctx context.Context, msg chat1.MessageUnboxed) (res chat1.MessageUnboxed, modified bool, err types.UnboxingError) { if !msg.IsValid() { return msg, false, nil } if msg.Valid().VerificationKey == nil { return msg, false, nil } // verify sender key revokedAt, ierr := b.ValidSenderKey(ctx, msg.Valid().ClientHeader.Sender, *msg.Valid().VerificationKey, msg.Valid().ServerHeader.Ctime) if ierr != nil { if ierr.IsPermanent() { return b.makeErrorMessageFromPieces(ctx, ierr, msg.GetMessageID(), msg.GetMessageType(), msg.Valid().ServerHeader.Ctime, msg.Valid().ClientHeader.Sender, msg.Valid().ClientHeader.SenderDevice, msg.Valid().ClientHeader.BotUID, msg.Valid().IsEphemeral(), msg.Valid().ExplodedBy(), msg.Valid().Etime()), true, nil } return msg, false, ierr } mvalid := msg.Valid() mvalid.SenderDeviceRevokedAt = revokedAt return chat1.NewMessageUnboxedWithValid(mvalid), revokedAt != nil, nil } func (b *Boxer) ResolveSkippedUnboxeds(ctx context.Context, msgs []chat1.MessageUnboxed) (res []chat1.MessageUnboxed, modifiedMap map[chat1.MessageID]bool, err types.UnboxingError) { modifiedMap = make(map[chat1.MessageID]bool) for _, msg := range msgs { rmsg, modified, err := b.ResolveSkippedUnboxed(ctx, msg) if err != nil { return res, modifiedMap, err } modifiedMap[rmsg.GetMessageID()] = modified res = append(res, rmsg) } return res, modifiedMap, nil } func (b *Boxer) unboxV2orV3orV4(ctx context.Context, boxed chat1.MessageBoxed, conv types.UnboxConversationInfo, baseEncryptionKey types.CryptKey, ephemeralKey types.EphemeralCryptKey) (*chat1.MessageUnboxedValid, types.UnboxingError) { if boxed.ServerHeader == nil { return nil, NewPermanentUnboxingError(errors.New("nil ServerHeader in MessageBoxed")) } // Compute the header hash headerHash, ierr := b.makeHeaderHash(boxed.HeaderCiphertext.AsSignEncrypted()) if ierr != nil { return nil, ierr } // Regular messages use the same encryption key for the header and for the // body. Exploding messages use a derived ephemeral key for the body. headerEncryptionKey, err := libkb.DeriveSymmetricKey( libkb.NaclSecretBoxKey(baseEncryptionKey.Material()), libkb.EncryptionReasonChatMessage) if err != nil { return nil, NewPermanentUnboxingError(err) } bodyEncryptionKey := headerEncryptionKey if boxed.IsEphemeral() { bodyEncryptionKey, err = libkb.DeriveFromSecret(ephemeralKey.Material(), libkb.DeriveReasonTeamEKExplodingChat) if err != nil { return nil, NewPermanentUnboxingError(err) } } // Validate verification key against unverified sender id. // Later it is asserted that the claimed and signing sender are the same. // ValidSenderKey uses the server-given ctime, but emits senderDeviceRevokedAt as a workaround. // See ValidSenderKey for details. if boxed.VerifyKey == nil { return nil, NewPermanentUnboxingError(libkb.NoKeyError{Msg: "sender key missing"}) } // This will be the message's signing key in the normal case, and the // sending device's encryption key in the pairwise MAC case. senderKeyToValidate := boxed.VerifyKey // When pairwise MACs are present (in practice only on exploding messages, // but in theory we support them anywhere), we validate the one that's // intended for our device, and error out if it's missing or invalid. If it // is valid, then we *don't* validate the message signing key. That is, // even though signEncryptOpen will check a signature in the end, we no // longer care what signing key it's using. if len(boxed.ClientHeader.PairwiseMacs) > 0 { if boxed.Version != chat1.MessageBoxedVersion_V3 && !bytes.Equal(boxed.VerifyKey, dummySigningKey().GetKID().ToBytes()) { return nil, NewPermanentUnboxingError(fmt.Errorf("expected dummy signing key (%s), got %s", dummySigningKey().GetKID(), hex.EncodeToString(boxed.VerifyKey))) } senderKeyToValidate, err = b.validatePairwiseMAC(ctx, boxed, conv, headerHash) if err != nil { // Return a transient error if possible return nil, b.detectPermanentError(conv, err, boxed.ClientHeader.TlfName) } } else if bytes.Equal(boxed.VerifyKey, dummySigningKey().GetKID().ToBytes()) { // Note that this can happen if the server is stripping MACs for some // reason, for example if you're testing against an out-of-date server // version. return nil, NewPermanentUnboxingError(fmt.Errorf("unexpected dummy signing key with no pairwise MACs present")) } // If we validated a pairwise MAC above, then senderKeyToValidate will be // the sender's device encryption key, instead of the VerifyKey from the // message. In this case, ValidSenderKey is just fetching revocation info for us. var senderDeviceRevokedAt *gregor1.Time switch globals.CtxUnboxMode(ctx) { case types.UnboxModeFull: senderDeviceRevokedAt, ierr = b.ValidSenderKey( ctx, boxed.ClientHeader.Sender, senderKeyToValidate, boxed.ServerHeader.Ctime) if ierr != nil { return nil, ierr } case types.UnboxModeQuick: // we skip this check in quick mode, the idea is we will do it later asynchonously so we can // deliver messges quicker to the UI. } // Open header and verify against VerifyKey headerPacked, err := b.signEncryptOpen(boxed.HeaderCiphertext.AsSignEncrypted(), headerEncryptionKey, boxed.VerifyKey, kbcrypto.SignaturePrefixChatMBv2) if err != nil { return nil, NewPermanentUnboxingError(err) } var headerVersioned chat1.HeaderPlaintext err = b.unmarshal(headerPacked, &headerVersioned) if err != nil { return nil, NewPermanentUnboxingError(err) } // Unversion header // Also check that the HeaderSignature field from MessageBoxed V1 is nil // This object has been signed clientHeader, bodyHashSigned, ierr := b.unversionHeaderMBV2(ctx, boxed.ServerHeader, headerVersioned) if ierr != nil { return nil, ierr } // Whether the body is missing (deleted) isBodyDeleted := (len(boxed.BodyCiphertext.E) == 0) // TODO We should check whether the body is allowed to have been deleted by checking // that there is in fact a message that deleted it. // We should fetch that message and check its signed body. // That involves fetching a message whose ID is not known here. // Verify body hash // The hash of the encrypted body must match that signed into the header. if !isBodyDeleted { ierr = b.verifyBodyHash(ctx, boxed.BodyCiphertext, bodyHashSigned) if ierr != nil { return nil, ierr } } // Compare the signed and unsigned header. // Checks that [Sender, SenderDevice] match, and other things. ierr = b.compareHeadersMBV2orV3(ctx, boxed.ClientHeader, clientHeader, boxed.Version) if ierr != nil { return nil, ierr } // Decrypt body // If the body is deleted, this is left blank. var body chat1.MessageBody if !isBodyDeleted { bodyPacked, err := b.open(boxed.BodyCiphertext, bodyEncryptionKey) if err != nil { return nil, NewPermanentUnboxingError(err) } var bodyVersioned chat1.BodyPlaintext err = b.unmarshal(bodyPacked, &bodyVersioned) if err != nil { return nil, NewPermanentUnboxingError(err) } // Unversion the body body, ierr = b.unversionBody(ctx, bodyVersioned) if ierr != nil { return nil, ierr } } // Get sender info // Any of (senderUsername, senderDeviceName, senderDeviceType) could be empty strings because of non-critical failures. senderUsername, senderDeviceName, senderDeviceType := b.getSenderInfoLocal( ctx, clientHeader.Sender, clientHeader.SenderDevice) // Get at mention usernames atMentions, atMentionUsernames, maybeRes, chanMention, channelNameMentions := b.getAtMentionInfo(ctx, clientHeader.Conv.Tlfid, clientHeader.Conv.TopicType, conv, body) clientHeader.HasPairwiseMacs = len(boxed.ClientHeader.PairwiseMacs) > 0 // create an unboxed message return &chat1.MessageUnboxedValid{ ClientHeader: clientHeader, ServerHeader: *boxed.ServerHeader, MessageBody: body, SenderUsername: senderUsername, SenderDeviceName: senderDeviceName, SenderDeviceType: senderDeviceType, BodyHash: bodyHashSigned, HeaderHash: headerHash, HeaderSignature: nil, VerificationKey: &senderKeyToValidate, SenderDeviceRevokedAt: senderDeviceRevokedAt, AtMentions: atMentions, AtMentionUsernames: atMentionUsernames, ChannelMention: chanMention, ChannelNameMentions: channelNameMentions, MaybeMentions: maybeRes, BotUsername: b.getBotInfoLocal(ctx, clientHeader.BotUID), Emojis: b.getEmojis(ctx, clientHeader.Conv.TopicType, body), }, nil } // Unversions a header. // Also check that the HeaderSignature field from MessageBoxed V1 is nil. // Therefore only for use with MessageBoxed V2. // Returns (header, bodyHash, err) func (b *Boxer) unversionHeaderMBV2(ctx context.Context, serverHeader *chat1.MessageServerHeader, headerVersioned chat1.HeaderPlaintext) (chat1.MessageClientHeaderVerified, []byte, types.UnboxingError) { if serverHeader == nil { return chat1.MessageClientHeaderVerified{}, nil, NewPermanentUnboxingError(errors.New("nil ServerHeader in MessageBoxed")) } rtime := gregor1.ToTime(b.clock.Now()) if serverHeader.Rtime != nil { rtime = *serverHeader.Rtime } headerVersion, err := headerVersioned.Version() if err != nil { return chat1.MessageClientHeaderVerified{}, nil, NewPermanentUnboxingError(err) } switch headerVersion { case chat1.HeaderPlaintextVersion_V1: hp := headerVersioned.V1() if hp.HeaderSignature != nil { return chat1.MessageClientHeaderVerified{}, nil, NewPermanentUnboxingError(fmt.Errorf("HeaderSignature non-nil in MBV2")) } return chat1.MessageClientHeaderVerified{ Conv: hp.Conv, TlfName: hp.TlfName, TlfPublic: hp.TlfPublic, MessageType: hp.MessageType, Prev: hp.Prev, Sender: hp.Sender, SenderDevice: hp.SenderDevice, MerkleRoot: hp.MerkleRoot, OutboxID: hp.OutboxID, OutboxInfo: hp.OutboxInfo, KbfsCryptKeysUsed: hp.KbfsCryptKeysUsed, EphemeralMetadata: hp.EphemeralMetadata, BotUID: hp.BotUID, Rtime: rtime, }, hp.BodyHash, nil // NOTE: When adding new versions here, you must also update // chat1/extras.go so MessageUnboxedError.ParseableVersion understands the // new max version default: return chat1.MessageClientHeaderVerified{}, nil, NewPermanentUnboxingError(NewHeaderVersionError(headerVersion, b.headerUnsupported(ctx, headerVersion, headerVersioned))) } } func (b *Boxer) unversionBody(ctx context.Context, bodyVersioned chat1.BodyPlaintext) (chat1.MessageBody, types.UnboxingError) { bodyVersion, err := bodyVersioned.Version() if err != nil { return chat1.MessageBody{}, NewPermanentUnboxingError(err) } switch bodyVersion { case chat1.BodyPlaintextVersion_V1: return bodyVersioned.V1().MessageBody, nil case chat1.BodyPlaintextVersion_V2: return bodyVersioned.V2().MessageBody, nil // NOTE: When adding new versions here, you must also update // chat1/extras.go so MessageUnboxedError.ParseableVersion understands the // new max version default: return chat1.MessageBody{}, NewPermanentUnboxingError(NewBodyVersionError(bodyVersion, b.bodyUnsupported(ctx, bodyVersion, bodyVersioned))) } } func (b *Boxer) verifyBodyHash(ctx context.Context, bodyEncrypted chat1.EncryptedData, bodyHashSigned []byte) types.UnboxingError { bodyHashObserved, err := b.makeBodyHash(bodyEncrypted) if err != nil { return err } if len(bodyHashSigned) == 0 { return NewPermanentUnboxingError(BodyHashInvalid{}) } if !libkb.SecureByteArrayEq(bodyHashObserved, bodyHashSigned) { return NewPermanentUnboxingError(BodyHashInvalid{}) } return nil } // Compare the unsigned and signed header for MessageBoxedVersion_V2. // The V1 and V2 checks are different methods because they are strict on slightly different things. // Confirm that fields in the server-supplied ClientHeader match what // we decrypt. It would be preferable if the server didn't supply this data // at all (so that we didn't have to worry about anyone trusting it // *before* we get to this check, for example), but since we have it we // need to check it. // The most important check here is that the Sender and SenderDevice match. // That is the only thing that gives the verification key used credibility. func (b *Boxer) compareHeadersMBV2orV3(ctx context.Context, hServer chat1.MessageClientHeader, hSigned chat1.MessageClientHeaderVerified, version chat1.MessageBoxedVersion) types.UnboxingError { // Conv if !hServer.Conv.Eq(hSigned.Conv) { return NewPermanentUnboxingError(NewHeaderMismatchError("Conv")) } // TlfName if hServer.TlfName != hSigned.TlfName { return NewPermanentUnboxingError(NewHeaderMismatchError("TlfName")) } // TlfPublic if hServer.TlfPublic != hSigned.TlfPublic { return NewPermanentUnboxingError(NewHeaderMismatchError("TlfPublic")) } // MessageType if hServer.MessageType != hSigned.MessageType { return NewPermanentUnboxingError(NewHeaderMismatchError("MessageType")) } // Note: Supersedes and Deletes are not checked because they are not // part of MessageClientHeaderVerified. // Prev if len(hServer.Prev) != len(hSigned.Prev) { return NewPermanentUnboxingError(NewHeaderMismatchError("Prev")) } for i, a := range hServer.Prev { b := hSigned.Prev[i] if !a.Eq(b) { return NewPermanentUnboxingError(NewHeaderMismatchError("Prev")) } } // Sender // This prevents someone from re-using a header for another sender if !hServer.Sender.Eq(hSigned.Sender) { return NewPermanentUnboxingError(NewHeaderMismatchError("Sender")) } // SenderDevice if !bytes.Equal(hServer.SenderDevice.Bytes(), hSigned.SenderDevice.Bytes()) { return NewPermanentUnboxingError(NewHeaderMismatchError("SenderDevice")) } // MerkleRoot if !hServer.MerkleRoot.Eq(hSigned.MerkleRoot) { return NewPermanentUnboxingError(NewHeaderMismatchError("MerkleRoot")) } if hSigned.MerkleRoot == nil { return NewPermanentUnboxingError(fmt.Errorf("missing MerkleRoot in chat message")) } // OutboxID if !hServer.OutboxID.Eq(hSigned.OutboxID) { return NewPermanentUnboxingError(NewHeaderMismatchError("OutboxID")) } // OutboxInfo if !hServer.OutboxInfo.Eq(hSigned.OutboxInfo) { return NewPermanentUnboxingError(NewHeaderMismatchError("OutboxInfo")) } // EphemeralMetadata (only present in V3 and greater) if version > chat1.MessageBoxedVersion_V2 && !hServer.EphemeralMetadata.Eq(hSigned.EphemeralMetadata) { return NewPermanentUnboxingError(NewHeaderMismatchError("EphemeralMetadata")) } // BotUID (only present in V3 and greater) if version > chat1.MessageBoxedVersion_V2 && !gregor1.UIDPtrEq(hServer.BotUID, hSigned.BotUID) { return NewPermanentUnboxingError(NewHeaderMismatchError("BotUID")) } return nil } func (b *Boxer) makeHeaderHash(headerSealed chat1.SignEncryptedData) (chat1.Hash, types.UnboxingError) { buf := bytes.Buffer{} err := binary.Write(&buf, binary.BigEndian, int32(headerSealed.V)) if err != nil { return nil, NewPermanentUnboxingError(err) } // Only the ciphertext at the end should be of variable length, otherwise // this hash could be ambiguous. if len(headerSealed.N) != signencrypt.NonceSize { return nil, NewPermanentUnboxingError(fmt.Errorf("unexpected nonce size, %d != %d", len(headerSealed.N), signencrypt.NonceSize)) } _, err = buf.Write(headerSealed.N) if err != nil { return nil, NewPermanentUnboxingError(err) } _, err = buf.Write(headerSealed.E) if err != nil { return nil, NewPermanentUnboxingError(err) } return b.hashV1(buf.Bytes()), nil } func (b *Boxer) makeBodyHash(bodyCiphertext chat1.EncryptedData) (chat1.Hash, types.UnboxingError) { buf := bytes.Buffer{} err := binary.Write(&buf, binary.BigEndian, int32(bodyCiphertext.V)) if err != nil { return nil, NewPermanentUnboxingError(err) } _, err = buf.Write(bodyCiphertext.N) if err != nil { return nil, NewPermanentUnboxingError(err) } _, err = buf.Write(bodyCiphertext.E) if err != nil { return nil, NewPermanentUnboxingError(err) } return b.hashV1(buf.Bytes()), nil } // unboxThread transforms a chat1.ThreadViewBoxed to a keybase1.ThreadView. func (b *Boxer) UnboxThread(ctx context.Context, boxed chat1.ThreadViewBoxed, conv types.UnboxConversationInfo) (thread chat1.ThreadView, err error) { thread = chat1.ThreadView{ Pagination: boxed.Pagination, } if thread.Messages, err = b.UnboxMessages(ctx, boxed.Messages, conv); err != nil { return chat1.ThreadView{}, err } return thread, nil } func (b *Boxer) getUsernameAndDevice(ctx context.Context, uid keybase1.UID, deviceID keybase1.DeviceID) (string, string, keybase1.DeviceTypeV2, error) { nun, devName, devType, err := globals.CtxUPAKFinder(ctx, b.G()).LookupUsernameAndDevice(ctx, uid, deviceID) if err != nil { return "", "", keybase1.DeviceTypeV2_NONE, err } return nun.String(), devName, devType, nil } func (b *Boxer) getUsername(ctx context.Context, uid keybase1.UID) (string, error) { name, err := b.G().GetUPAKLoader().LookupUsername(ctx, uid) if err != nil { return "", err } return name.String(), nil } // Any of (senderUsername, senderDeviceName, senderDeviceType) could be empty strings because of non-critical failures. // This first tries to username and device info, falling back to just username, falling back to empty strings. // The reason for this soft error handling is that a permanent failure would be inappropriate, a transient // failure could cause an entire thread not to load, and loading the names of revoked devices may not work this way. // This deserves to be reconsidered. func (b *Boxer) getSenderInfoLocal(ctx context.Context, uid1 gregor1.UID, deviceID1 gregor1.DeviceID) (senderUsername string, senderDeviceName string, senderDeviceType keybase1.DeviceTypeV2) { if uid1.IsNil() { b.Debug(ctx, "unable to fetch sender and device information: nil UID") return "", "", "" } if b.testingGetSenderInfoLocal != nil { b.assertInTest() return b.testingGetSenderInfoLocal(ctx, uid1, deviceID1) } uid := keybase1.UID(uid1.String()) did := keybase1.DeviceID(deviceID1.String()) username, deviceName, deviceType, err := b.getUsernameAndDevice(ctx, uid, did) if err != nil { b.Debug(ctx, "unable to fetch sender and device information: UID: %s deviceID: %s", uid1, deviceID1) // try to just get username username, err = b.getUsername(ctx, uid) if err != nil { b.Debug(ctx, "failed to fetch sender username after initial error: err: %s", err.Error()) } } return username, deviceName, deviceType } func (b *Boxer) getBotInfoLocal(ctx context.Context, uid *gregor1.UID) string { if uid == nil { return "" } kbuid := keybase1.UID(uid.String()) username, err := b.getUsername(ctx, kbuid) if err != nil { b.Debug(ctx, "failed to fetch bot username: %v", err) return "" } return username } func (b *Boxer) getEmojis(ctx context.Context, topicType chat1.TopicType, body chat1.MessageBody) (res []chat1.HarvestedEmoji) { if topicType != chat1.TopicType_CHAT { return nil } emojis := body.GetEmojis() if emojis == nil { return nil } res = make([]chat1.HarvestedEmoji, 0, len(emojis)) for _, emoji := range emojis { res = append(res, emoji) } return res } func (b *Boxer) getAtMentionInfo(ctx context.Context, tlfID chat1.TLFID, topicType chat1.TopicType, conv types.UnboxConversationInfo, body chat1.MessageBody) (atMentions []gregor1.UID, atMentionUsernames []string, maybeRes []chat1.MaybeMention, chanMention chat1.ChannelMention, channelNameMentions []chat1.ChannelNameMention) { if topicType != chat1.TopicType_CHAT { // only care about chat conversations for these mentions return atMentions, atMentionUsernames, maybeRes, chanMention, channelNameMentions } chanMention = chat1.ChannelMention_NONE typ, err := body.MessageType() if err != nil { return nil, nil, nil, chanMention, nil } uid := gregor1.UID(b.G().GetEnv().GetUID().ToBytes()) tcs := b.G().TeamChannelSource var userAtMentions []chat1.KnownUserMention switch typ { case chat1.MessageType_TEXT: userAtMentions, maybeRes, chanMention = utils.GetTextAtMentionedItems(ctx, b.G(), uid, conv.GetConvID(), body.Text(), nil, &b.DebugLabeler) if conv.GetMembersType() == chat1.ConversationMembersType_TEAM { channelNameMentions = utils.ParseChannelNameMentions(ctx, body.Text().Body, uid, tlfID, tcs) } case chat1.MessageType_ATTACHMENT: userAtMentions, maybeRes, chanMention = utils.ParseAtMentionedItems(ctx, b.G(), body.Attachment().GetTitle(), body.Attachment().UserMentions, nil) if conv.GetMembersType() == chat1.ConversationMembersType_TEAM { channelNameMentions = utils.ParseChannelNameMentions(ctx, body.Attachment().GetTitle(), uid, tlfID, tcs) } case chat1.MessageType_FLIP: if topicType == chat1.TopicType_CHAT { userAtMentions, maybeRes, chanMention = utils.ParseAtMentionedItems(ctx, b.G(), body.Flip().Text, body.Flip().UserMentions, nil) } case chat1.MessageType_EDIT: userAtMentions, maybeRes, chanMention = utils.ParseAtMentionedItems(ctx, b.G(), body.Edit().Body, body.Edit().UserMentions, nil) if conv.GetMembersType() == chat1.ConversationMembersType_TEAM { channelNameMentions = utils.ParseChannelNameMentions(ctx, body.Edit().Body, uid, tlfID, tcs) } case chat1.MessageType_SYSTEM: atMentions, chanMention, channelNameMentions = utils.SystemMessageMentions(ctx, b.G(), uid, body.System()) case chat1.MessageType_REACTION: targetUID := body.Reaction().TargetUID if targetUID != nil { atMentions = []gregor1.UID{*targetUID} } default: return nil, nil, nil, chanMention, nil } for _, uat := range userAtMentions { atMentions = append(atMentions, uat.Uid) } usernames := make(map[string]bool) for _, uid := range atMentions { name, err := b.G().GetUPAKLoader().LookupUsername(ctx, keybase1.UID(uid.String())) if err != nil { continue } usernames[name.String()] = true } for u := range usernames { atMentionUsernames = append(atMentionUsernames, u) } return atMentions, atMentionUsernames, maybeRes, chanMention, channelNameMentions } func (b *Boxer) UnboxMessages(ctx context.Context, boxed []chat1.MessageBoxed, conv types.UnboxConversationInfo) (unboxed []chat1.MessageUnboxed, err error) { defer b.Trace(ctx, &err, "UnboxMessages: %s, boxed: %d", conv.GetConvID(), len(boxed))() // First stamp all of the messages as received now := gregor1.ToTime(b.clock.Now()) for i, msg := range boxed { msg.ServerHeader.Rtime = &now boxed[i] = msg } boxCh := make(chan chat1.MessageBoxed) eg, ctx := errgroup.WithContext(ctx) eg.Go(func() error { defer close(boxCh) for _, msg := range boxed { select { case boxCh <- msg: case <-ctx.Done(): return ctx.Err() } } return nil }) var resLock sync.Mutex numUnboxThreads := 2 for i := 0; i < numUnboxThreads; i++ { eg.Go(func() error { for msg := range boxCh { decmsg, err := b.UnboxMessage(ctx, msg, conv, nil) if err != nil { return err } resLock.Lock() unboxed = append(unboxed, decmsg) resLock.Unlock() } return nil }) } if err := eg.Wait(); err != nil { return unboxed, err } sort.Sort(utils.ByMsgUnboxedMsgID(unboxed)) return unboxed, nil } // If no error then MerkleRoot is non-nil. func (b *Boxer) latestMerkleRoot(ctx context.Context) (*chat1.MerkleRoot, error) { merkleClient := b.G().GetMerkleClient() if merkleClient == nil { return nil, fmt.Errorf("no MerkleClient available") } mr, err := merkleClient.FetchRootFromServer(b.G().MetaContext(ctx), libkb.ChatBoxerMerkleFreshness) if err != nil { return nil, err } if mr == nil { return nil, fmt.Errorf("No merkle root available for chat header") } merkleRoot := mr.ToInfo() return &merkleRoot, nil } var dummySigningKeyPtr *libkb.NaclSigningKeyPair var dummySigningKeyOnce sync.Once // We use this constant key when we already have pairwiseMACs providing // authentication. Creating a keypair requires a curve multiply, so we cache it // here, in case someone uses it in a tight loop. func dummySigningKey() libkb.NaclSigningKeyPair { dummySigningKeyOnce.Do(func() { var allZeroSecretKey [libkb.NaclSigningKeySecretSize]byte dummyKeypair, err := libkb.MakeNaclSigningKeyPairFromSecret(allZeroSecretKey) if err != nil { panic("errors in key generation should be impossible: " + err.Error()) } dummySigningKeyPtr = &dummyKeypair }) return *dummySigningKeyPtr } func (b *Boxer) GetEncryptionInfo(ctx context.Context, msg *chat1.MessagePlaintext, membersType chat1.ConversationMembersType, signingKeyPair libkb.NaclSigningKeyPair) (res types.BoxerEncryptionInfo, err error) { tlfName := msg.ClientHeader.TlfName version, err := b.GetBoxedVersion(*msg) if err != nil { return res, err } encryptionKey, nameInfo, err := globals.CtxKeyFinder(ctx, b.G()).FindForEncryption(ctx, tlfName, msg.ClientHeader.Conv.Tlfid, membersType, msg.ClientHeader.TlfPublic, msg.ClientHeader.BotUID) if err != nil { return res, NewBoxingCryptKeysError(err) } msg.ClientHeader.TlfName = nameInfo.CanonicalName // If the message is exploding, load the ephemeral key, and tweak the // version. Make sure we're not using MessageBoxedVersion_V1, since that // doesn't support exploding messages. var ephemeralKey types.EphemeralCryptKey var pairwiseMACRecipients []keybase1.KID if msg.IsEphemeral() { ephemeralKey, err = globals.CtxKeyFinder(ctx, b.G()).EphemeralKeyForEncryption( b.G().MetaContext(ctx), tlfName, msg.ClientHeader.Conv.Tlfid, membersType, msg.ClientHeader.TlfPublic, msg.ClientHeader.BotUID) if err != nil { return res, NewBoxingCryptKeysError(err) } // V3 is "V2 plus support for exploding messages", and V4 is "V3 plus // support for pairwise MACs". Thus we'll bump all exploding messages // from V2 to V3, and all MAC'd messages from V3 to V4. Eventually we // can deprecate the old versions and remove these branches, once // support is widespread. if version == chat1.MessageBoxedVersion_V2 { version = chat1.MessageBoxedVersion_V3 } // If this is a team conversation, and the team is small enough, load // the list of pairwise MAC recipients. Note that this is all the // devices in the team, not just those that can read the current // teamEK. There are a few reasons for doing it this way: // - It's probably better performance. Including all devices // makes the message bigger and takes more Curve25519 ops, but // it means we only need to reference the UPAK cache. To get // the set of devices-that-are-not-stale, we'd need to ask the // server and pay the cost of a network round trip. We could // introduce yet another caching layer, but EKs change more // frequently than devices in general. // - It leaves us more flexibility in the future. If say we // introduce a best-effort rekey mechanism for ephmeral keys, // existing pairwise MACs will Just Work™ after a rekey. shouldPairwiseMAC, recipients, err := globals.CtxKeyFinder(ctx, b.G()).ShouldPairwiseMAC( ctx, tlfName, msg.ClientHeader.Conv.Tlfid, membersType, msg.ClientHeader.TlfPublic) if err != nil { return res, err } else if shouldPairwiseMAC { if len(recipients) == 0 { return res, fmt.Errorf("unexpected empty pairwise recipients list") } pairwiseMACRecipients = recipients // As noted above, bump the version to V4 when we're MAC'ing. if version == chat1.MessageBoxedVersion_V3 { version = chat1.MessageBoxedVersion_V4 } // Replace the signing key with a dummy. Using the real signing key // would sabotage the repudiability that pairwise MACs are // providing. We could avoid signing entirely, but this approach // keeps the difference between the two modes very small. signingKeyPair = dummySigningKey() } } return types.BoxerEncryptionInfo{ Key: encryptionKey, EphemeralKey: ephemeralKey, PairwiseMACRecipients: pairwiseMACRecipients, Version: version, SigningKeyPair: signingKeyPair, }, nil } func (b *Boxer) GetBoxedVersion(msg chat1.MessagePlaintext) (chat1.MessageBoxedVersion, error) { version := CurrentMessageBoxedVersion if b.boxVersionForTesting != nil { version = *b.boxVersionForTesting } if msg.IsEphemeral() && version == chat1.MessageBoxedVersion_V1 { return version, fmt.Errorf("cannot use exploding messages with V1") } return version, nil } // BoxMessage encrypts a keybase1.MessagePlaintext into a chat1.MessageBoxed. It // finds the most recent key for the TLF. func (b *Boxer) BoxMessage(ctx context.Context, msg chat1.MessagePlaintext, membersType chat1.ConversationMembersType, signingKeyPair libkb.NaclSigningKeyPair, info *types.BoxerEncryptionInfo) (res chat1.MessageBoxed, err error) { defer b.Trace(ctx, &err, "BoxMessage")() tlfName := msg.ClientHeader.TlfName if len(tlfName) == 0 { return res, NewBoxingError("blank TLF name given", true) } version, err := b.GetBoxedVersion(msg) if err != nil { return res, err } if err = b.attachMerkleRoot(ctx, &msg, version); err != nil { return res, err } if info == nil { info = new(types.BoxerEncryptionInfo) if *info, err = b.GetEncryptionInfo(ctx, &msg, membersType, signingKeyPair); err != nil { return res, err } if len(msg.ClientHeader.TlfName) == 0 { msg := fmt.Sprintf("blank TLF name received: original: %s canonical: %s", tlfName, msg.ClientHeader.TlfName) return res, NewBoxingError(msg, true) } } boxed, err := b.box(ctx, msg, info.Key, info.EphemeralKey, info.SigningKeyPair, info.Version, info.PairwiseMACRecipients) if err != nil { return res, NewBoxingError(err.Error(), true) } return boxed, nil } // Attach a merkle root to the message to send. // Modifies msg. // For MessageBoxedV1 makes sure there is no MR. // For MessageBoxedV2 attaches a MR that is no more out of date than ChatBoxerMerkleFreshness. func (b *Boxer) attachMerkleRoot(ctx context.Context, msg *chat1.MessagePlaintext, version chat1.MessageBoxedVersion) error { switch version { case chat1.MessageBoxedVersion_V1: if msg.ClientHeader.MerkleRoot != nil { return NewBoxingError("cannot send v1 message with merkle root", true) } case chat1.MessageBoxedVersion_V2, chat1.MessageBoxedVersion_V3, chat1.MessageBoxedVersion_V4: merkleRoot, err := b.latestMerkleRoot(ctx) if err != nil { return NewBoxingError(err.Error(), false) } msg.ClientHeader.MerkleRoot = merkleRoot if msg.ClientHeader.MerkleRoot == nil { return NewBoxingError("cannot send message without merkle root", false) } default: return fmt.Errorf("attachMerkleRoot unrecognized version: %s", version) } return nil } func (b *Boxer) preBoxCheck(ctx context.Context, messagePlaintext chat1.MessagePlaintext) error { typ, err := messagePlaintext.MessageBody.MessageType() if err != nil { return err } e := func(format string, args ...interface{}) error { return errors.New(fmt.Sprintf("malformed %v message: ", typ) + fmt.Sprintf(format, args...)) } switch typ { case chat1.MessageType_DELETEHISTORY: body := messagePlaintext.MessageBody.Deletehistory() dhHeader := messagePlaintext.ClientHeader.DeleteHistory if dhHeader == nil { return e("missing header") } if *dhHeader != body { return e("header-body mismatch") } default: if messagePlaintext.ClientHeader.DeleteHistory != nil { return e("cannot have delete-history header") } } return nil } func (b *Boxer) box(ctx context.Context, messagePlaintext chat1.MessagePlaintext, encryptionKey types.CryptKey, ephemeralKey types.EphemeralCryptKey, signingKeyPair libkb.NaclSigningKeyPair, version chat1.MessageBoxedVersion, pairwiseMACRecipients []keybase1.KID) (res chat1.MessageBoxed, err error) { if err = b.preBoxCheck(ctx, messagePlaintext); err != nil { return res, err } switch version { case chat1.MessageBoxedVersion_V1: if res, err = b.boxV1(messagePlaintext, encryptionKey, signingKeyPair); err != nil { b.Debug(ctx, "error boxing message version: %v", version) } return res, err // V3 is the same as V2, except that it indicates exploding message // support. V4 is the same as V3, except that it signs with the zero key // when pairwise MACs are included. case chat1.MessageBoxedVersion_V2, chat1.MessageBoxedVersion_V3, chat1.MessageBoxedVersion_V4: if res, err = b.boxV2orV3orV4(ctx, messagePlaintext, encryptionKey, ephemeralKey, signingKeyPair, version, pairwiseMACRecipients); err != nil { b.Debug(ctx, "error boxing message version: %v", version) } return res, err default: return res, fmt.Errorf("invalid version for boxing: %v", version) } } // boxMessageWithKeys encrypts and signs a keybase1.MessagePlaintext into a // chat1.MessageBoxed given a keybase1.CryptKey. func (b *Boxer) boxV1(messagePlaintext chat1.MessagePlaintext, key types.CryptKey, signingKeyPair libkb.NaclSigningKeyPair) (res chat1.MessageBoxed, err error) { body := chat1.BodyPlaintextV1{ MessageBody: messagePlaintext.MessageBody, } plaintextBody := chat1.NewBodyPlaintextWithV1(body) encryptedBody, err := b.seal(plaintextBody, libkb.NaclSecretBoxKey(key.Material())) if err != nil { return res, err } // create the v1 header, adding hash bodyHash := b.hashV1(encryptedBody.E) if messagePlaintext.ClientHeader.MerkleRoot != nil { return res, fmt.Errorf("cannot box v1 message with merkle root") } header := chat1.HeaderPlaintextV1{ Conv: messagePlaintext.ClientHeader.Conv, TlfName: messagePlaintext.ClientHeader.TlfName, TlfPublic: messagePlaintext.ClientHeader.TlfPublic, MessageType: messagePlaintext.ClientHeader.MessageType, Prev: messagePlaintext.ClientHeader.Prev, Sender: messagePlaintext.ClientHeader.Sender, SenderDevice: messagePlaintext.ClientHeader.SenderDevice, MerkleRoot: nil, // MerkleRoot cannot be sent in MBv1 messages BodyHash: bodyHash[:], OutboxInfo: messagePlaintext.ClientHeader.OutboxInfo, OutboxID: messagePlaintext.ClientHeader.OutboxID, KbfsCryptKeysUsed: messagePlaintext.ClientHeader.KbfsCryptKeysUsed, } // sign the header and insert the signature sig, err := b.signMarshal(header, signingKeyPair, kbcrypto.SignaturePrefixChatMBv1) if err != nil { return res, err } header.HeaderSignature = &sig // create a plaintext header plaintextHeader := chat1.NewHeaderPlaintextWithV1(header) encryptedHeader, err := b.seal(plaintextHeader, libkb.NaclSecretBoxKey(key.Material())) if err != nil { return res, err } return chat1.MessageBoxed{ Version: chat1.MessageBoxedVersion_V1, ClientHeader: messagePlaintext.ClientHeader, BodyCiphertext: *encryptedBody, HeaderCiphertext: encryptedHeader.AsSealed(), KeyGeneration: key.Generation(), }, nil } func makeOnePairwiseMAC(private libkb.NaclDHKeyPrivate, public libkb.NaclDHKeyPublic, input []byte) []byte { privKeyBytes := [32]byte(private) pubKeyBytes := [32]byte(public) var rawShared [32]byte box.Precompute(&rawShared, &pubKeyBytes, &privKeyBytes) derivedShared, err := libkb.DeriveFromSecret(rawShared, libkb.DeriveReasonChatPairwiseMAC) if err != nil { panic(err) // key derivation should never fail } hmacState := hmac.New(sha256.New, derivedShared[:]) _, _ = hmacState.Write(input) return hmacState.Sum(nil) } func (b *Boxer) makeAllPairwiseMACs(ctx context.Context, headerSealed chat1.SignEncryptedData, recipients []keybase1.KID) (macs map[keybase1.KID][]byte, err error) { defer b.G().CTrace(ctx, fmt.Sprintf("makeAllPairwiseMACs with %d recipients", len(recipients)), &err)() pairwiseMACs := map[keybase1.KID][]byte{} headerHash, ierr := b.makeHeaderHash(headerSealed) if ierr != nil { return nil, ierr } deviceKeyNacl, err := b.G().ActiveDevice.NaclEncryptionKey() if err != nil { return nil, err } for _, recipientKID := range recipients { recipientKeyNacl, err := libkb.ImportDHKeypairFromKID(recipientKID) if err != nil { return nil, err } pairwiseMACs[recipientKID] = makeOnePairwiseMAC(*deviceKeyNacl.Private, recipientKeyNacl.Public, headerHash) } return pairwiseMACs, nil } func (b *Boxer) versionBody(ctx context.Context, messagePlaintext chat1.MessagePlaintext) chat1.BodyPlaintext { switch messagePlaintext.ClientHeader.MessageType { case chat1.MessageType_PIN: return chat1.NewBodyPlaintextWithV2(chat1.BodyPlaintextV2{ MessageBody: messagePlaintext.MessageBody, }) default: return chat1.NewBodyPlaintextWithV1(chat1.BodyPlaintextV1{ MessageBody: messagePlaintext.MessageBody, }) } } // V3 is just V2 but with exploding messages support. V4 is just V3, but it // signs with the zero key when pairwise MACs are included. func (b *Boxer) boxV2orV3orV4(ctx context.Context, messagePlaintext chat1.MessagePlaintext, baseEncryptionKey types.CryptKey, ephemeralKey types.EphemeralCryptKey, signingKeyPair libkb.NaclSigningKeyPair, version chat1.MessageBoxedVersion, pairwiseMACRecipients []keybase1.KID) (res chat1.MessageBoxed, err error) { if messagePlaintext.ClientHeader.MerkleRoot == nil { return res, NewBoxingError("cannot send message without merkle root", false) } headerEncryptionKey, err := libkb.DeriveSymmetricKey( libkb.NaclSecretBoxKey(baseEncryptionKey.Material()), libkb.EncryptionReasonChatMessage) if err != nil { return res, err } // Regular messages use the same encryption key for the header and for the // body. Exploding messages use a derived ephemeral key for the body. bodyEncryptionKey := headerEncryptionKey if messagePlaintext.IsEphemeral() { bodyEncryptionKey, err = libkb.DeriveFromSecret(ephemeralKey.Material(), libkb.DeriveReasonTeamEKExplodingChat) if err != nil { return res, err } // The MessagePlaintext supplied by the caller has a Lifetime, but we // expect the Generation is left uninitialized, and we set it here. messagePlaintext.ClientHeader.EphemeralMetadata.Generation = ephemeralKey.Generation() } bodyVersioned := b.versionBody(ctx, messagePlaintext) bodyEncrypted, err := b.seal(bodyVersioned, bodyEncryptionKey) if err != nil { return res, err } bodyHash, err := b.makeBodyHash(*bodyEncrypted) if err != nil { return res, err } // create the v1 header, adding hash headerVersioned := chat1.NewHeaderPlaintextWithV1(chat1.HeaderPlaintextV1{ Conv: messagePlaintext.ClientHeader.Conv, TlfName: messagePlaintext.ClientHeader.TlfName, TlfPublic: messagePlaintext.ClientHeader.TlfPublic, MessageType: messagePlaintext.ClientHeader.MessageType, Prev: messagePlaintext.ClientHeader.Prev, Sender: messagePlaintext.ClientHeader.Sender, SenderDevice: messagePlaintext.ClientHeader.SenderDevice, BodyHash: bodyHash, MerkleRoot: messagePlaintext.ClientHeader.MerkleRoot, OutboxInfo: messagePlaintext.ClientHeader.OutboxInfo, OutboxID: messagePlaintext.ClientHeader.OutboxID, KbfsCryptKeysUsed: messagePlaintext.ClientHeader.KbfsCryptKeysUsed, EphemeralMetadata: messagePlaintext.ClientHeader.EphemeralMetadata, BotUID: messagePlaintext.ClientHeader.BotUID, // In MessageBoxed.V2 HeaderSignature is nil. HeaderSignature: nil, }) // signencrypt the header headerSealed, err := b.signEncryptMarshal(headerVersioned, headerEncryptionKey, signingKeyPair, kbcrypto.SignaturePrefixChatMBv2) if err != nil { return res, err } // Make pairwise MACs if there are any pairwise recipients supplied. Note // that we still sign+encrypt with a signing key above. If we want // repudiability, it's the caller's responsibility to provide a zero // signing key or similar. Signing with a real key and also MAC'ing is // redundant, but it will let us test the MAC code in prod in a backwards // compatible way. if len(pairwiseMACRecipients) > 0 { pairwiseMACs, err := b.makeAllPairwiseMACs(ctx, headerSealed, pairwiseMACRecipients) if err != nil { return res, err } messagePlaintext.ClientHeader.PairwiseMacs = pairwiseMACs } // verify verifyKey := signingKeyPair.GetBinaryKID() return chat1.MessageBoxed{ Version: version, ServerHeader: nil, ClientHeader: messagePlaintext.ClientHeader, HeaderCiphertext: headerSealed.AsSealed(), BodyCiphertext: *bodyEncrypted, VerifyKey: verifyKey, KeyGeneration: baseEncryptionKey.Generation(), }, nil } // seal encrypts data into chat1.EncryptedData. func (b *Boxer) seal(data interface{}, key libkb.NaclSecretBoxKey) (*chat1.EncryptedData, error) { s, err := b.marshal(data) if err != nil { return nil, err } var nonce [libkb.NaclDHNonceSize]byte if _, err := rand.Read(nonce[:]); err != nil { return nil, err } var encKey [libkb.NaclSecretBoxKeySize]byte = key sealed := secretbox.Seal(nil, s, &nonce, &encKey) enc := &chat1.EncryptedData{ V: 1, E: sealed, N: nonce[:], } return enc, nil } // open decrypts chat1.EncryptedData. func (b *Boxer) open(data chat1.EncryptedData, key libkb.NaclSecretBoxKey) ([]byte, error) { if len(data.N) != libkb.NaclDHNonceSize { return nil, libkb.DecryptBadNonceError{} } var nonce [libkb.NaclDHNonceSize]byte copy(nonce[:], data.N) plain, ok := secretbox.Open(nil, data.E, &nonce, (*[32]byte)(&key)) if !ok { return nil, libkb.DecryptOpenError{} } return plain, nil } // signMarshal signs data with a NaclSigningKeyPair, returning a chat1.SignatureInfo. // It marshals data before signing. func (b *Boxer) signMarshal(data interface{}, kp libkb.NaclSigningKeyPair, prefix kbcrypto.SignaturePrefix) (chat1.SignatureInfo, error) { encoded, err := b.marshal(data) if err != nil { return chat1.SignatureInfo{}, err } return b.sign(encoded, kp, prefix) } // signEncryptMarshal signencrypts data given an encryption and signing key, returning a chat1.SignEncryptedData. // It marshals data before signing. func (b *Boxer) signEncryptMarshal(data interface{}, encryptionKey libkb.NaclSecretBoxKey, signingKeyPair libkb.NaclSigningKeyPair, prefix kbcrypto.SignaturePrefix) (chat1.SignEncryptedData, error) { encoded, err := b.marshal(data) if err != nil { return chat1.SignEncryptedData{}, err } return b.signEncrypt(encoded, encryptionKey, signingKeyPair, prefix) } // sign signs msg with a NaclSigningKeyPair, returning a chat1.SignatureInfo. func (b *Boxer) sign(msg []byte, kp libkb.NaclSigningKeyPair, prefix kbcrypto.SignaturePrefix) (chat1.SignatureInfo, error) { sig, err := kp.SignV2(msg, prefix) if err != nil { return chat1.SignatureInfo{}, err } sigInfo := chat1.SignatureInfo{ V: sig.Version, S: sig.Sig[:], K: sig.Kid, } if b.testingSignatureMangle != nil { b.assertInTest() sigInfo.S = b.testingSignatureMangle(sigInfo.S) } return sigInfo, nil } // signEncrypt signencrypts msg. func (b *Boxer) signEncrypt(msg []byte, encryptionKey libkb.NaclSecretBoxKey, signingKeyPair libkb.NaclSigningKeyPair, prefix kbcrypto.SignaturePrefix) (chat1.SignEncryptedData, error) { if signingKeyPair.Private == nil { return chat1.SignEncryptedData{}, libkb.NoSecretKeyError{} } var nonce [signencrypt.NonceSize]byte if _, err := rand.Read(nonce[:]); err != nil { return chat1.SignEncryptedData{}, err } var encKey [signencrypt.SecretboxKeySize]byte = encryptionKey var signKey [ed25519.PrivateKeySize]byte = *signingKeyPair.Private signEncryptedBytes := signencrypt.SealWhole( msg, &encKey, &signKey, prefix, &nonce) signEncryptedInfo := chat1.SignEncryptedData{ V: 1, E: signEncryptedBytes, N: nonce[:], } if b.testingSignatureMangle != nil { b.assertInTest() signEncryptedInfo.E = b.testingSignatureMangle(signEncryptedInfo.E) } return signEncryptedInfo, nil } // signEncryptOpen opens and verifies chat1.SignEncryptedData. func (b *Boxer) signEncryptOpen(data chat1.SignEncryptedData, encryptionKey libkb.NaclSecretBoxKey, verifyKID []byte, prefix kbcrypto.SignaturePrefix) ([]byte, error) { var encKey [signencrypt.SecretboxKeySize]byte = encryptionKey verifyKey := kbcrypto.KIDToNaclSigningKeyPublic(verifyKID) if verifyKey == nil { return nil, kbcrypto.BadKeyError{} } var verKey [ed25519.PublicKeySize]byte = *verifyKey var nonce [signencrypt.NonceSize]byte if copy(nonce[:], data.N) != signencrypt.NonceSize { return nil, libkb.DecryptBadNonceError{} } plain, err := signencrypt.OpenWhole(data.E, &encKey, &verKey, prefix, &nonce) if err != nil { return nil, err } return plain, nil } type verifyMessageRes struct { senderDeviceRevokedAt *gregor1.Time validationKey []byte } // verifyMessage checks that a message is valid. // Only works on MessageBoxedVersion_V1 func (b *Boxer) verifyMessageV1(ctx context.Context, header chat1.HeaderPlaintext, msg chat1.MessageBoxed, skipBodyVerification bool) (verifyMessageRes, types.UnboxingError) { headerVersion, err := header.Version() if err != nil { return verifyMessageRes{}, NewPermanentUnboxingError(err) } switch headerVersion { case chat1.HeaderPlaintextVersion_V1: return b.verifyMessageHeaderV1(ctx, header.V1(), msg, skipBodyVerification) // NOTE: When adding new versions here, you must also update // chat1/extras.go so MessageUnboxedError.ParseableVersion understands the // new max version default: return verifyMessageRes{}, NewPermanentUnboxingError(NewHeaderVersionError(headerVersion, b.headerUnsupported(ctx, headerVersion, header))) } } // verifyMessageHeaderV1 checks the body hash, header signature, and signing key validity. func (b *Boxer) verifyMessageHeaderV1(ctx context.Context, header chat1.HeaderPlaintextV1, msg chat1.MessageBoxed, skipBodyVerification bool) (verifyMessageRes, types.UnboxingError) { if !skipBodyVerification { // check body hash bh := b.hashV1(msg.BodyCiphertext.E) if !libkb.SecureByteArrayEq(bh[:], header.BodyHash) { return verifyMessageRes{}, NewPermanentUnboxingError(BodyHashInvalid{}) } } // check key validity // ValidSenderKey uses the server-given ctime, but emits senderDeviceRevokedAt as a workaround. // See ValidSenderKey for details. var revoked *gregor1.Time validationKey := header.HeaderSignature.K switch globals.CtxUnboxMode(ctx) { case types.UnboxModeFull: var ierr types.UnboxingError revoked, ierr = b.ValidSenderKey(ctx, header.Sender, header.HeaderSignature.K, msg.ServerHeader.Ctime) if ierr != nil { return verifyMessageRes{}, ierr } default: // Nothing to do. } // check signature hcopy := header hcopy.HeaderSignature = nil hpack, err := b.marshal(hcopy) if err != nil { return verifyMessageRes{}, NewPermanentUnboxingError(err) } if !b.verify(hpack, *header.HeaderSignature, kbcrypto.SignaturePrefixChatMBv1) { return verifyMessageRes{}, NewPermanentUnboxingError(libkb.BadSigError{E: "header signature invalid"}) } return verifyMessageRes{ senderDeviceRevokedAt: revoked, validationKey: validationKey, }, nil } // verify verifies the signature of data using SignatureInfo. func (b *Boxer) verify(data []byte, si chat1.SignatureInfo, prefix kbcrypto.SignaturePrefix) bool { sigInfo := kbcrypto.NaclSigInfo{ Version: si.V, Prefix: prefix, Kid: si.K, Payload: data, } copy(sigInfo.Sig[:], si.S) _, err := sigInfo.Verify() return (err == nil) } // ValidSenderKey checks that the key was active for sender at ctime. // This trusts the server for ctime, so a colluding server could use a revoked key and this check erroneously pass. // But (revoked != nil) if the key was ever revoked, so that is irrespective of ctime. // Returns (validAtCtime, revoked, err) func (b *Boxer) ValidSenderKey(ctx context.Context, sender gregor1.UID, key []byte, ctime gregor1.Time) (revoked *gregor1.Time, unboxErr types.UnboxingError) { var found, deleted bool var revokedAt *keybase1.KeybaseTime validAtCtime := true if b.testingValidSenderKey != nil { b.assertInTest() return b.testingValidSenderKey(ctx, sender, key, ctime) } defer func() { if unboxErr == nil { if !found { unboxErr = NewPermanentUnboxingError(libkb.NoKeyError{Msg: "sender key not found"}) } else if !validAtCtime { unboxErr = NewPermanentUnboxingError(libkb.NoKeyError{ Msg: "key invalid for sender at message ctime", }) } } }() kbSender, err := keybase1.UIDFromString(hex.EncodeToString(sender.Bytes())) if err != nil { return nil, NewPermanentUnboxingError(err) } kid := keybase1.KIDFromSlice(key) ctime2 := gregor1.FromTime(ctime) cachedUserLoader := globals.CtxUPAKFinder(ctx, b.G()) if cachedUserLoader == nil { return nil, NewTransientUnboxingError(fmt.Errorf("no CachedUserLoader available in context")) } found, revokedAt, deleted, err = cachedUserLoader.CheckKIDForUID(ctx, kbSender, kid) if err != nil { return nil, NewTransientUnboxingError(err) } if !found { return nil, nil } if deleted { b.Debug(ctx, "sender %s key %s was deleted", kbSender, kid) // Set the key as being revoked since the beginning of time, so all messages will get labeled // as suspect zeroTime := gregor1.Time(0) return &zeroTime, nil } if revokedAt != nil { if revokedAt.Unix.IsZero() { return nil, NewPermanentUnboxingError(fmt.Errorf("zero clock time on expired key")) } t := b.keybase1KeybaseTimeToTime(*revokedAt) revokedTime := gregor1.ToTime(t) revoked = &revokedTime validAtCtime = t.After(ctime2) } return revoked, nil } func (b *Boxer) keybase1KeybaseTimeToTime(t1 keybase1.KeybaseTime) time.Time { // u is in milliseconds u := int64(t1.Unix) t2 := time.Unix(u/1e3, (u%1e3)*1e6) return t2 } func (b *Boxer) marshal(v interface{}) ([]byte, error) { mh := codec.MsgpackHandle{WriteExt: true} var data []byte enc := codec.NewEncoderBytes(&data, &mh) if err := enc.Encode(v); err != nil { return nil, err } return data, nil } func (b *Boxer) unmarshal(data []byte, v interface{}) error { mh := codec.MsgpackHandle{WriteExt: true} dec := codec.NewDecoderBytes(data, &mh) return dec.Decode(&v) } func (b *Boxer) assertInTest() { b.log().Warning("Using TESTING jig. Not suitable for normal use.") if flag.Lookup("test.v") == nil { panic("testing jig installed in normal mode") } } func hashSha256V1(data []byte) chat1.Hash { sum := sha256.Sum256(data) return sum[:] } // See note on compareHeadersMBV2orV3. func (b *Boxer) compareHeadersMBV1(ctx context.Context, hServer chat1.MessageClientHeader, hSigned chat1.MessageClientHeaderVerified) types.UnboxingError { // Conv if !hServer.Conv.Eq(hSigned.Conv) { return NewPermanentUnboxingError(NewHeaderMismatchError("Conv")) } // TlfName if hServer.TlfName != hSigned.TlfName { return NewPermanentUnboxingError(NewHeaderMismatchError("TlfName")) } // TlfPublic if hServer.TlfPublic != hSigned.TlfPublic { return NewPermanentUnboxingError(NewHeaderMismatchError("TlfPublic")) } // MessageType if hServer.MessageType != hSigned.MessageType { return NewPermanentUnboxingError(NewHeaderMismatchError("MessageType")) } // Note: Supersedes, Deletes, and some other fields are not checked because they are not // part of MessageClientHeaderVerified. // Prev if len(hServer.Prev) != len(hSigned.Prev) { return NewPermanentUnboxingError(NewHeaderMismatchError("Prev")) } for i, a := range hServer.Prev { b := hSigned.Prev[i] if !a.Eq(b) { return NewPermanentUnboxingError(NewHeaderMismatchError("Prev")) } } // Sender // This prevents someone from re-using a header for another sender if !hServer.Sender.Eq(hSigned.Sender) { return NewPermanentUnboxingError(NewHeaderMismatchError("Sender")) } // SenderDevice if !bytes.Equal(hServer.SenderDevice.Bytes(), hSigned.SenderDevice.Bytes()) { return NewPermanentUnboxingError(NewHeaderMismatchError("SenderDevice")) } // _Don't_ check that the MerkleRoot's match. // The signed MerkleRoot should be nil as it was not part of the protocol // when clients were writing MBV1. But we just allow anything here. // There are V1 messages in the wild with hServer.MerkleRoot set but nothing signed. // OutboxID, OutboxInfo: Left unchecked as I'm not sure whether these hold in V1 messages. return nil } func (b *Boxer) CompareTlfNames(ctx context.Context, tlfName1, tlfName2 string, membersType chat1.ConversationMembersType, tlfPublic bool) (bool, error) { get1 := func(tlfName string, tlfPublic bool) (string, error) { nameInfo, err := CreateNameInfoSource(ctx, b.G(), membersType).LookupID(ctx, tlfName, tlfPublic) if err != nil { return "", err } return nameInfo.CanonicalName, nil } c1, err := get1(tlfName1, tlfPublic) if err != nil { return false, err } c2, err := get1(tlfName2, tlfPublic) if err != nil { return false, err } return c1 == c2, nil }