| 1 | package release |
| 2 | |
| 3 | import ( |
| 4 | "crypto/ed25519" |
| 5 | "encoding/base64" |
| 6 | "encoding/json" |
| 7 | "fmt" |
| 8 | ) |
| 9 | |
| 10 | // PointerAccount reads the account a channel pointer names WITHOUT verifying |
| 11 | // it — "" for a pointer from before the binding existed (discussion #220). |
| 12 | // For planning only: `accounts migrate` asks it whether a pointer can travel |
| 13 | // as bytes or has to be re-signed for its new account, and it trusts nothing |
| 14 | // it learns here. Whatever it then writes is verified like any pointer. |
| 15 | func PointerAccount(wrapper []byte) (string, error) { |
| 16 | var sw signedWrapper |
| 17 | if err := json.Unmarshal(wrapper, &sw); err != nil { |
| 18 | return "", fmt.Errorf("pointer wrapper: %v", err) |
| 19 | } |
| 20 | payload, err := base64.StdEncoding.DecodeString(sw.Payload) |
| 21 | if err != nil { |
| 22 | return "", fmt.Errorf("pointer payload: %v", err) |
| 23 | } |
| 24 | var p ChannelPointer |
| 25 | if err := json.Unmarshal(payload, &p); err != nil { |
| 26 | return "", fmt.Errorf("pointer payload: %v", err) |
| 27 | } |
| 28 | return p.Account, nil |
| 29 | } |
| 30 | |
| 31 | // RebindPointer is how a channel pointer moves between accounts. A pointer |
| 32 | // names the account it was signed for, so a byte copy into another account's |
| 33 | // prefix no longer verifies there — which is the point of the binding, and |
| 34 | // what `accounts migrate` would otherwise trip over. The pointer is verified |
| 35 | // under fromAcct with root's public half, then re-signed by root for toAcct |
| 36 | // with every other field kept, PromotedAt included. |
| 37 | // |
| 38 | // Deterministic on purpose: Ed25519 signatures are, and nothing is |
| 39 | // re-stamped, so the same source bytes always rebind to the same bytes. The |
| 40 | // migration compares destination bytes against this output to decide whether |
| 41 | // a pointer has already been copied, and a fresh timestamp would make every |
| 42 | // re-run look like a disagreement. |
| 43 | // |
| 44 | // A pointer that names no account (written before the binding) or already |
| 45 | // names toAcct is returned as is: it verifies at the destination unchanged. |
| 46 | func RebindPointer(root ed25519.PrivateKey, wrapper []byte, fromAcct, toAcct, app, channel string) ([]byte, error) { |
| 47 | if !ValidAccount(toAcct) { |
| 48 | return nil, fmt.Errorf("rebind: invalid account %q", toAcct) |
| 49 | } |
| 50 | p, err := verifyPointer(root.Public().(ed25519.PublicKey), wrapper, fromAcct, app, channel) |
| 51 | if err != nil { |
| 52 | return nil, fmt.Errorf("rebind %s/%s: %w", app, channel, err) |
| 53 | } |
| 54 | if p.Account == "" || p.Account == toAcct { |
| 55 | return wrapper, nil |
| 56 | } |
| 57 | p.Account = toAcct |
| 58 | if err := validatePointer(p, toAcct, app, channel); err != nil { |
| 59 | return nil, err |
| 60 | } |
| 61 | payload, err := json.MarshalIndent(p, "", " ") |
| 62 | if err != nil { |
| 63 | return nil, err |
| 64 | } |
| 65 | b64 := base64.StdEncoding.EncodeToString(payload) |
| 66 | return json.MarshalIndent(signedWrapper{ |
| 67 | Payload: b64, |
| 68 | Sig: base64.StdEncoding.EncodeToString(ed25519.Sign(root, []byte(b64))), |
| 69 | }, "", " ") |
| 70 | } |
| 71 | |