package release import ( "crypto/ed25519" "encoding/base64" "encoding/json" "fmt" ) // PointerAccount reads the account a channel pointer names WITHOUT verifying // it — "" for a pointer from before the binding existed (discussion #220). // For planning only: `accounts migrate` asks it whether a pointer can travel // as bytes or has to be re-signed for its new account, and it trusts nothing // it learns here. Whatever it then writes is verified like any pointer. func PointerAccount(wrapper []byte) (string, error) { var sw signedWrapper if err := json.Unmarshal(wrapper, &sw); err != nil { return "", fmt.Errorf("pointer wrapper: %v", err) } payload, err := base64.StdEncoding.DecodeString(sw.Payload) if err != nil { return "", fmt.Errorf("pointer payload: %v", err) } var p ChannelPointer if err := json.Unmarshal(payload, &p); err != nil { return "", fmt.Errorf("pointer payload: %v", err) } return p.Account, nil } // RebindPointer is how a channel pointer moves between accounts. A pointer // names the account it was signed for, so a byte copy into another account's // prefix no longer verifies there — which is the point of the binding, and // what `accounts migrate` would otherwise trip over. The pointer is verified // under fromAcct with root's public half, then re-signed by root for toAcct // with every other field kept, PromotedAt included. // // Deterministic on purpose: Ed25519 signatures are, and nothing is // re-stamped, so the same source bytes always rebind to the same bytes. The // migration compares destination bytes against this output to decide whether // a pointer has already been copied, and a fresh timestamp would make every // re-run look like a disagreement. // // A pointer that names no account (written before the binding) or already // names toAcct is returned as is: it verifies at the destination unchanged. func RebindPointer(root ed25519.PrivateKey, wrapper []byte, fromAcct, toAcct, app, channel string) ([]byte, error) { if !ValidAccount(toAcct) { return nil, fmt.Errorf("rebind: invalid account %q", toAcct) } p, err := verifyPointer(root.Public().(ed25519.PublicKey), wrapper, fromAcct, app, channel) if err != nil { return nil, fmt.Errorf("rebind %s/%s: %w", app, channel, err) } if p.Account == "" || p.Account == toAcct { return wrapper, nil } p.Account = toAcct if err := validatePointer(p, toAcct, app, channel); err != nil { return nil, err } payload, err := json.MarshalIndent(p, "", " ") if err != nil { return nil, err } b64 := base64.StdEncoding.EncodeToString(payload) return json.MarshalIndent(signedWrapper{ Payload: b64, Sig: base64.StdEncoding.EncodeToString(ed25519.Sign(root, []byte(b64))), }, "", " ") }