Skip to content

Commit 3f830ca

Browse files
authored
fix: handle error 99 (member count mismatch) (#240)
* fix: handle error 99 (member count mismatch) * fix: restore plaintext fallback for unavailable group keys
1 parent 39f9ba9 commit 3f830ca

6 files changed

Lines changed: 101 additions & 508 deletions

File tree

‎pkg/connector/creategroup.go‎

Lines changed: 61 additions & 133 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@ import (
1616
var (
1717
_ bridgev2.GroupCreatingNetworkAPI = (*LineClient)(nil)
1818

19-
getLastE2EEPublicKeysWithClient = func(client *line.Client, req line.GetLastE2EEPublicKeysRequest) (map[string]line.E2EEPeerPublicKey, error) {
20-
return client.GetLastE2EEPublicKeys(req)
19+
getLastE2EEPublicKeysWithClient = func(client *line.Client, chatMid string) (map[string]line.E2EEPeerPublicKey, error) {
20+
return client.GetLastE2EEPublicKeys(chatMid)
2121
}
2222
)
2323

@@ -47,8 +47,7 @@ func (lc *LineClient) CreateGroup(ctx context.Context, params *bridgev2.GroupCre
4747
Int("participants", len(participantMids)).
4848
Msg("LINE group chat created")
4949

50-
// Cache the member list so auto-registration can fall back to it
51-
// when GetChats withMembers returns empty data.
50+
// Cache the member list for group name generation.
5251
groupMembers := make([]string, 0, len(participantMids)+1)
5352
groupMembers = append(groupMembers, lc.Mid)
5453
groupMembers = append(groupMembers, participantMids...)
@@ -66,7 +65,7 @@ func (lc *LineClient) CreateGroup(ctx context.Context, params *bridgev2.GroupCre
6665
// Registration is best-effort: an incomplete E2EE member list leaves the
6766
// chat on its plaintext fallback without aborting group creation.
6867
if lc.E2EE != nil && len(participantMids) > 0 {
69-
if err := lc.registerGroupKey(ctx, chat.ChatMid, participantMids); err != nil {
68+
if err := lc.registerGroupKey(ctx, chat.ChatMid); err != nil {
7069
lc.UserLogin.Bridge.Log.Warn().Err(err).
7170
Str("chat_mid", chat.ChatMid).
7271
Msg("Failed to register E2EE group key, continuing without E2EE")
@@ -131,153 +130,82 @@ func (lc *LineClient) CreateGroup(ctx context.Context, params *bridgev2.GroupCre
131130
}, nil
132131
}
133132

134-
func (lc *LineClient) groupKeyMemberMIDs(chatMid string, members []string) []string {
135-
otherMembers := make([]string, 0, len(members))
136-
seen := make(map[string]struct{}, len(members))
137-
for _, mid := range members {
138-
if !isUserMID(mid) || lc.isOwnMID(mid) || mid == chatMid {
139-
continue
140-
}
141-
if _, ok := seen[mid]; ok {
142-
continue
143-
}
144-
seen[mid] = struct{}{}
145-
otherMembers = append(otherMembers, mid)
146-
}
147-
return otherMembers
133+
type groupKeyCrypto interface {
134+
GenerateGroupKey() (int, error)
135+
WrapGroupKeyForMember(string, int) (string, error)
148136
}
149137

150-
func (lc *LineClient) resolveGroupMemberPublicKeys(ctx context.Context, client *line.Client, chatMid string, members []string) (*line.Client, map[string]line.E2EEPeerPublicKey, error) {
151-
req := line.GetLastE2EEPublicKeysRequest{ChatMid: chatMid, Members: members}
152-
client, pubKeys, batchErr := callLineResultUsing(lc, ctx, client, func(client *line.Client) (map[string]line.E2EEPeerPublicKey, error) {
153-
return getLastE2EEPublicKeysWithClient(client, req)
154-
})
155-
if batchErr != nil {
156-
if ctx.Err() != nil || lc.isSessionInvalidated() || line.IsAuthError(batchErr) {
157-
return client, nil, batchErr
158-
}
159-
lc.UserLogin.Bridge.Log.Warn().Err(batchErr).
160-
Str("chat_mid", chatMid).
161-
Int("members", len(members)).
162-
Msg("Batch GetLastE2EEPublicKeys failed, resolving member keys individually")
163-
}
164-
if pubKeys == nil {
165-
pubKeys = make(map[string]line.E2EEPeerPublicKey, len(members))
138+
func (lc *LineClient) registerGroupKey(ctx context.Context, chatMid string) error {
139+
if lc.E2EE == nil {
140+
return fmt.Errorf("%w: E2EE manager not initialized", line.ErrNoUsableE2EEGroupKey)
166141
}
142+
return lc.registerGroupKeyWithCrypto(ctx, chatMid, lc.E2EE)
143+
}
167144

168-
for _, mid := range members {
169-
if pk, ok := pubKeys[mid]; ok && pk.KeyID > 0 && pk.KeyData != "" {
170-
continue
145+
func (lc *LineClient) registerGroupKeyWithCrypto(ctx context.Context, chatMid string, crypto groupKeyCrypto) error {
146+
client := lc.newClient()
147+
for attempt := 0; ; attempt++ {
148+
if err := ctx.Err(); err != nil {
149+
return err
171150
}
172-
173-
var res *line.E2EEPublicKey
151+
// LINE returns the current registration recipients, including the caller.
152+
// GetChats and Matrix membership can be incomplete or stale.
153+
var pubKeys map[string]line.E2EEPeerPublicKey
174154
var err error
175-
client, res, err = callLineResultUsing(lc, ctx, client, func(client *line.Client) (*line.E2EEPublicKey, error) {
176-
return negotiateE2EEPublicKeyWithClient(client, mid)
155+
client, pubKeys, err = callLineResultUsing(lc, ctx, client, func(client *line.Client) (map[string]line.E2EEPeerPublicKey, error) {
156+
return getLastE2EEPublicKeysWithClient(client, chatMid)
177157
})
178158
if err != nil {
179-
if ctx.Err() != nil || lc.isSessionInvalidated() || line.IsAuthError(err) {
180-
return client, nil, fmt.Errorf("negotiate E2EE key for member %s: %w", mid, err)
181-
}
182-
if line.IsNoUsableE2EEPublicKey(err) {
183-
return client, nil, fmt.Errorf("%w: member %s has Letter Sealing disabled", line.ErrNoUsableE2EEGroupKey, mid)
184-
}
185-
return client, nil, fmt.Errorf("%w: negotiate E2EE key for member %s: %w", line.ErrNoUsableE2EEGroupKey, mid, err)
159+
return fmt.Errorf("getLastE2EEPublicKeys failed: %w", err)
186160
}
187-
if res == nil || res.PublicKey == "" {
188-
return client, nil, fmt.Errorf("%w: member %s returned no public key", line.ErrNoUsableE2EEGroupKey, mid)
161+
if len(pubKeys) == 0 {
162+
return fmt.Errorf("%w: no registration members returned for group key", line.ErrNoUsableE2EEGroupKey)
189163
}
190-
keyID, err := res.KeyID.Int64()
191-
if err != nil {
192-
return client, nil, fmt.Errorf("%w: parse E2EE key ID for member %s: %w", line.ErrNoUsableE2EEGroupKey, mid, err)
164+
if _, ok := pubKeys[lc.Mid]; !ok {
165+
return fmt.Errorf("%w: group key registration members do not include caller", line.ErrNoUsableE2EEGroupKey)
193166
}
194-
if keyID <= 0 {
195-
return client, nil, fmt.Errorf("%w: member %s returned invalid key ID %d", line.ErrNoUsableE2EEGroupKey, mid, keyID)
167+
for mid, pk := range pubKeys {
168+
if !isUserMID(mid) {
169+
return fmt.Errorf("invalid E2EE group member MID")
170+
}
171+
if pk.KeyID <= 0 || pk.KeyData == "" {
172+
return fmt.Errorf("%w: incomplete E2EE group member public key", line.ErrNoUsableE2EEGroupKey)
173+
}
196174
}
197-
pubKeys[mid] = line.E2EEPeerPublicKey{KeyID: int(keyID), KeyData: res.PublicKey}
198-
}
199-
200-
return client, pubKeys, nil
201-
}
202-
203-
// registerGroupKey generates a random 32-byte group key, wraps it for each member
204-
// using ECDH + AES-256-CBC, and registers it with the LINE server so all members
205-
// can decrypt group messages.
206-
func (lc *LineClient) registerGroupKey(ctx context.Context, chatMid string, members []string) error {
207-
members = lc.groupKeyMemberMIDs(chatMid, members)
208-
if lc.E2EE == nil {
209-
return fmt.Errorf("%w: E2EE manager not initialized", line.ErrNoUsableE2EEGroupKey)
210-
}
211175

212-
client := lc.newClient()
213-
214-
// Batch responses can be partial without returning an error. Resolve every
215-
// missing member individually so registration arrays retain the server's
216-
// expected member count.
217-
pubKeys := make(map[string]line.E2EEPeerPublicKey, len(members))
218-
if len(members) > 0 {
219-
var err error
220-
client, pubKeys, err = lc.resolveGroupMemberPublicKeys(ctx, client, chatMid, members)
176+
groupKeyID, err := crypto.GenerateGroupKey()
221177
if err != nil {
178+
return fmt.Errorf("failed to generate group key: %w", err)
179+
}
180+
apiMembers := make([]string, 0, len(pubKeys))
181+
keyIDs := make([]int, 0, len(pubKeys))
182+
encryptedKeys := make([]string, 0, len(pubKeys))
183+
for mid, pk := range pubKeys {
184+
encryptedKey, err := crypto.WrapGroupKeyForMember(pk.KeyData, groupKeyID)
185+
if err != nil {
186+
return fmt.Errorf("wrap group key for member: %w", err)
187+
}
188+
apiMembers = append(apiMembers, mid)
189+
keyIDs = append(keyIDs, pk.KeyID)
190+
encryptedKeys = append(encryptedKeys, encryptedKey)
191+
}
192+
if err := ctx.Err(); err != nil {
222193
return err
223194
}
224-
}
225195

226-
// Generate group key in WASM (same approach as LINE Chrome Extension).
227-
// The generated key is a Curve25519Key object stored in the WASM module.
228-
groupKeyID, err := lc.E2EE.GenerateGroupKey()
229-
if err != nil {
230-
return fmt.Errorf("failed to generate group key: %w", err)
231-
}
232-
233-
// Wrap the group key for each member that has a public key
234-
apiMembers := make([]string, 0, len(members))
235-
keyIds := make([]int, 0, len(members))
236-
encryptedKeys := make([]string, 0, len(members))
237-
238-
for _, mid := range members {
239-
pk, ok := pubKeys[mid]
240-
if !ok || pk.KeyID <= 0 || pk.KeyData == "" {
241-
return fmt.Errorf("%w: incomplete E2EE public key for member %s", line.ErrNoUsableE2EEGroupKey, mid)
196+
client, err = lc.callLineUsing(ctx, client, func(client *line.Client) error {
197+
return client.RegisterE2EEGroupKey(1, chatMid, apiMembers, keyIDs, encryptedKeys)
198+
})
199+
if attempt == 0 && line.IsE2EEGroupMemberMismatch(err) {
200+
lc.UserLogin.Bridge.Log.Debug().Int("members", len(pubKeys)).
201+
Msg("Group membership changed during key registration, refreshing member keys")
202+
continue
242203
}
243-
244-
encryptedKey, err := lc.E2EE.WrapGroupKeyForMember(pk.KeyData, groupKeyID)
245204
if err != nil {
246-
return fmt.Errorf("wrap group key for member %s: %w", mid, err)
205+
return fmt.Errorf("registerE2EEGroupKey failed: %w", err)
247206
}
248-
249-
apiMembers = append(apiMembers, mid)
250-
keyIds = append(keyIds, pk.KeyID)
251-
encryptedKeys = append(encryptedKeys, encryptedKey)
252-
}
253-
254-
// LINE's registerE2EEGroupKey requires the caller's own key entry as well — without it the
255-
// server rejects the request with "empty caller key". The Chrome extension wraps the group
256-
// key for every member returned by getLastE2EEPublicKeys, which includes the caller. Mirror
257-
// that by wrapping the group key for our own public key and appending ourselves.
258-
selfRawID, selfPub, err := lc.E2EE.MyPublicKey()
259-
if err != nil {
260-
return fmt.Errorf("get own E2EE key: %w", err)
207+
lc.UserLogin.Bridge.Log.Info().Str("chat_mid", chatMid).
208+
Int("members", len(apiMembers)).Msg("Registered E2EE group key")
209+
return nil
261210
}
262-
selfEncryptedKey, err := lc.E2EE.WrapGroupKeyForMember(selfPub, groupKeyID)
263-
if err != nil {
264-
return fmt.Errorf("wrap group key for self: %w", err)
265-
}
266-
apiMembers = append(apiMembers, lc.Mid)
267-
keyIds = append(keyIds, selfRawID)
268-
encryptedKeys = append(encryptedKeys, selfEncryptedKey)
269-
270-
_, err = lc.callLineUsing(ctx, client, func(client *line.Client) error {
271-
return client.RegisterE2EEGroupKey(1, chatMid, apiMembers, keyIds, encryptedKeys)
272-
})
273-
if err != nil {
274-
return fmt.Errorf("registerE2EEGroupKey failed: %w", err)
275-
}
276-
277-
lc.UserLogin.Bridge.Log.Info().
278-
Str("chat_mid", chatMid).
279-
Int("members", len(apiMembers)).
280-
Msg("Registered E2EE group key")
281-
282-
return nil
283211
}

0 commit comments

Comments
 (0)