feat(pia): add PIA login-and-add WireGuard outbounds (#6272)

* feat(pia): add login-and-add WireGuard outbounds (#2)

* fix(pia): keep PIA outbounds identifiable after the editor strips hostname

The outbound editor drops piaHostname, so last-segment matching failed for hyphenated servers. Identify rows by the computed tag, re-encrypt stored tokens onto the active key, skip unusable catalog rows, and always release the catalog refresh latch.
This commit is contained in:
Masterain
2026-08-23 05:11:06 +08:00
committed by GitHub
parent a3e617215c
commit bd6a6aba43
73 changed files with 4095 additions and 31 deletions
+30 -10
View File
@@ -97,12 +97,26 @@ func IsEncrypted(stored string) bool { return strings.HasPrefix(stored, encPrefi
// Encrypt returns plaintext in ModeOff or row-bound enc:v1 ciphertext otherwise.
// Empty and already-valid encrypted values remain unchanged.
func (c *Codec) Encrypt(nodeID int, plaintext string) (string, error) {
return c.EncryptBound(aad(nodeID), plaintext)
}
// Decrypt passes legacy plaintext through; enc: values must authenticate and
// are never reinterpreted as plaintext after an error.
func (c *Codec) Decrypt(nodeID int, stored string) (string, error) {
pt, err := c.DecryptBound(aad(nodeID), stored)
if err != nil {
return "", fmt.Errorf("nodetoken: node %d: %w", nodeID, err)
}
return pt, nil
}
// EncryptBound is Encrypt with an explicit AAD (e.g. settings/pia_token).
func (c *Codec) EncryptBound(bound []byte, plaintext string) (string, error) {
if c.mode == ModeOff || plaintext == "" {
return plaintext, nil
}
if IsEncrypted(plaintext) {
// Validate it actually decrypts for this node; if so keep verbatim.
if _, err := c.Decrypt(nodeID, plaintext); err != nil {
if _, err := c.DecryptBound(bound, plaintext); err != nil {
return "", fmt.Errorf("nodetoken: refusing to store undecryptable ciphertext: %w", err)
}
return plaintext, nil
@@ -119,14 +133,13 @@ func (c *Codec) Encrypt(nodeID int, plaintext string) (string, error) {
if _, err := rand.Read(nonce); err != nil {
return "", err
}
ct := gcm.Seal(nil, nonce, []byte(plaintext), aad(nodeID))
ct := gcm.Seal(nil, nonce, []byte(plaintext), bound)
blob := append(nonce, ct...)
return encScheme + c.ring.ActiveID + ":" + base64.RawURLEncoding.EncodeToString(blob), nil
}
// Decrypt passes legacy plaintext through; enc: values must authenticate and
// are never reinterpreted as plaintext after an error.
func (c *Codec) Decrypt(nodeID int, stored string) (string, error) {
// DecryptBound is Decrypt with an explicit AAD.
func (c *Codec) DecryptBound(bound []byte, stored string) (string, error) {
if c.mode == ModeOff {
return stored, nil
}
@@ -159,9 +172,9 @@ func (c *Codec) Decrypt(nodeID int, stored string) (string, error) {
if err != nil {
return "", err
}
pt, err := gcm.Open(nil, blob[:nonceLen], blob[nonceLen:], aad(nodeID))
pt, err := gcm.Open(nil, blob[:nonceLen], blob[nonceLen:], bound)
if err != nil {
return "", fmt.Errorf("nodetoken: authentication failed for node %d: %w", nodeID, err)
return "", fmt.Errorf("nodetoken: authentication failed: %w", err)
}
return string(pt), nil
}
@@ -232,5 +245,12 @@ func get() *Codec {
// Encrypt/Decrypt/Enabled operate on the process-wide codec.
func Encrypt(nodeID int, plaintext string) (string, error) { return get().Encrypt(nodeID, plaintext) }
func Decrypt(nodeID int, stored string) (string, error) { return get().Decrypt(nodeID, stored) }
func Enabled() bool { return get().Enabled() }
func Active() *Codec { return get() }
func EncryptBound(bound []byte, plaintext string) (string, error) {
return get().EncryptBound(bound, plaintext)
}
func DecryptBound(bound []byte, stored string) (string, error) {
return get().DecryptBound(bound, stored)
}
func Enabled() bool { return get().Enabled() }
func Active() *Codec { return get() }
@@ -49,6 +49,23 @@ func TestAADBindsToNode(t *testing.T) {
// Decrypting under a different node id must fail (ciphertext bound to row).
if _, err := c.Decrypt(8, enc); err == nil {
t.Fatal("expected AAD mismatch error decrypting under wrong node id")
} else if !strings.Contains(err.Error(), "node 8") || !strings.Contains(err.Error(), "authentication failed") {
t.Fatalf("wrong-node decrypt error: %v", err)
}
}
func TestAADBindsSettingsApartFromNodes(t *testing.T) {
c, _ := NewCodec(ModeRequired, testRing(t, "k1", "k1"))
enc, err := c.EncryptBound([]byte("settings/pia_token"), "tok")
if err != nil {
t.Fatal(err)
}
if _, err := c.Decrypt(1, enc); err == nil {
t.Fatal("settings/pia_token ciphertext must not decrypt under nodes/api_token/1")
}
pt, err := c.DecryptBound([]byte("settings/pia_token"), enc)
if err != nil || pt != "tok" {
t.Fatalf("pia AAD round-trip: %q err=%v", pt, err)
}
}