@@ -16,8 +16,8 @@ import (
1616var (
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