Skip to content

Commit ab42cac

Browse files
committed
Set default credentials path to user config
1 parent 5e62ba8 commit ab42cac

3 files changed

Lines changed: 54 additions & 33 deletions

File tree

cmd/doubletake/main.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ func main() {
2020
target := flag.String("target", "", "Apple TV IP address or hostname (skip discovery)")
2121
port := flag.Int("port", 7000, "AirPlay port")
2222
pin := flag.String("pin", "", "4-digit PIN for pairing (shown on Apple TV)")
23-
credFile := flag.String("creds", airplay.DefaultCredentialsFile, "Path to saved pairing credentials")
23+
credFile := flag.String("creds", airplay.DefaultCredentialsPath(), "Path to saved pairing credentials")
2424
forcePair := flag.Bool("pair", false, "Force new pairing even if credentials exist")
2525
width := flag.Int("width", 1920, "Stream width")
2626
height := flag.Int("height", 1080, "Stream height")

internal/airplay/credentials.go

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,8 @@ type SavedCredentials struct {
1616
Ed25519Seed []byte `json:"ed25519_seed"` // 32-byte seed (private key is derived from this)
1717
}
1818

19-
const DefaultCredentialsFile = "airplay-credentials.json"
20-
21-
// DefaultCredentialStorePath returns ~/.config/doubletake/credentials.json.
22-
func DefaultCredentialStorePath() string {
19+
// DefaultCredentialsPath returns ~/.config/doubletake/credentials.json.
20+
func DefaultCredentialsPath() string {
2321
dir := os.Getenv("XDG_CONFIG_HOME")
2422
if dir == "" {
2523
home, _ := os.UserHomeDir()
@@ -45,8 +43,9 @@ func SaveCredentials(path string, pairingID string, pub ed25519.PublicKey, priv
4543
return nil
4644
}
4745

48-
// LoadCredentials reads pairing credentials from disk (legacy single-device format).
49-
// Returns nil, nil if the file doesn't exist.
46+
// LoadCredentials reads pairing credentials from disk.
47+
// Supports both the legacy single-device format and the multi-device map format
48+
// written by CredentialStore. Returns nil, nil if the file doesn't exist.
5049
func LoadCredentials(path string) (*SavedCredentials, error) {
5150
data, err := os.ReadFile(path)
5251
if err != nil {
@@ -55,15 +54,33 @@ func LoadCredentials(path string) (*SavedCredentials, error) {
5554
}
5655
return nil, fmt.Errorf("read credentials: %w", err)
5756
}
57+
58+
// Try multi-device format first (map[string]*SavedCredentials)
59+
var multi map[string]*SavedCredentials
60+
if err := json.Unmarshal(data, &multi); err == nil && len(multi) > 0 {
61+
for _, creds := range multi {
62+
if creds != nil && creds.PairingID != "" {
63+
return creds, nil
64+
}
65+
}
66+
}
67+
68+
// Fall back to legacy single-device format
5869
var creds SavedCredentials
5970
if err := json.Unmarshal(data, &creds); err != nil {
6071
return nil, fmt.Errorf("unmarshal credentials: %w", err)
6172
}
73+
if creds.PairingID == "" {
74+
return nil, nil
75+
}
6276
return &creds, nil
6377
}
6478

6579
// Ed25519Keys reconstructs the key pair from saved credentials.
6680
func (c *SavedCredentials) Ed25519Keys() (ed25519.PublicKey, ed25519.PrivateKey) {
81+
if len(c.Ed25519Seed) != ed25519.SeedSize {
82+
return nil, nil
83+
}
6784
priv := ed25519.NewKeyFromSeed(c.Ed25519Seed)
6885
return ed25519.PublicKey(c.Ed25519Public), priv
6986
}

internal/daemon/daemon.go

Lines changed: 30 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -103,8 +103,8 @@ type Daemon struct {
103103
// New creates a new Daemon with the given configuration.
104104
func New(cfg Config) (*Daemon, error) {
105105
credPath := cfg.CredFile
106-
if credPath == "" || credPath == airplay.DefaultCredentialsFile {
107-
credPath = airplay.DefaultCredentialStorePath()
106+
if credPath == "" {
107+
credPath = airplay.DefaultCredentialsPath()
108108
}
109109
cs, err := airplay.NewCredentialStore(credPath)
110110
if err != nil {
@@ -452,34 +452,38 @@ func (d *Daemon) connectAndStream(ctx context.Context, target string, port int,
452452
savedCreds := d.credStore.Lookup(deviceID)
453453
if savedCreds != nil {
454454
pub, priv := savedCreds.Ed25519Keys()
455-
client.PairingID = savedCreds.PairingID
456-
client.PairKeys = &airplay.PairKeys{
457-
Ed25519Public: pub,
458-
Ed25519Private: priv,
459-
}
460-
if err := client.PairVerify(ctx); err != nil {
461-
log.Printf("[daemon] pair-verify with saved creds failed: %v, trying transient pairing", err)
462-
// Reconnect for fresh pairing attempt
463-
client.Close()
464-
client = airplay.NewAirPlayClient(target, port)
465-
if err := client.Connect(ctx); err != nil {
466-
setErr(fmt.Sprintf("reconnect failed: %v", err))
467-
return
468-
}
469-
if _, err := client.GetInfo(); err != nil {
470-
setErr(fmt.Sprintf("get info after reconnect failed: %v", err))
471-
return
455+
if priv == nil {
456+
log.Printf("[daemon] saved credentials have invalid keys, skipping pair-verify")
457+
} else {
458+
client.PairingID = savedCreds.PairingID
459+
client.PairKeys = &airplay.PairKeys{
460+
Ed25519Public: pub,
461+
Ed25519Private: priv,
472462
}
473-
// Try transient (no-PIN) pairing as fallback
474-
if err := client.Pair(ctx, ""); err != nil {
475-
log.Printf("[daemon] transient pairing also failed: %v", err)
463+
if err := client.PairVerify(ctx); err != nil {
464+
log.Printf("[daemon] pair-verify with saved creds failed: %v, trying transient pairing", err)
465+
// Reconnect for fresh pairing attempt
466+
client.Close()
467+
client = airplay.NewAirPlayClient(target, port)
468+
if err := client.Connect(ctx); err != nil {
469+
setErr(fmt.Sprintf("reconnect failed: %v", err))
470+
return
471+
}
472+
if _, err := client.GetInfo(); err != nil {
473+
setErr(fmt.Sprintf("get info after reconnect failed: %v", err))
474+
return
475+
}
476+
// Try transient (no-PIN) pairing as fallback
477+
if err := client.Pair(ctx, ""); err != nil {
478+
log.Printf("[daemon] transient pairing also failed: %v", err)
479+
} else {
480+
paired = true
481+
log.Printf("[daemon] transient pairing succeeded for %s", info.Name)
482+
}
476483
} else {
477484
paired = true
478-
log.Printf("[daemon] transient pairing succeeded for %s", info.Name)
485+
log.Printf("[daemon] pair-verify succeeded for %s", info.Name)
479486
}
480-
} else {
481-
paired = true
482-
log.Printf("[daemon] pair-verify succeeded for %s", info.Name)
483487
}
484488
}
485489
}

0 commit comments

Comments
 (0)