@@ -24,19 +24,16 @@ type KeyPair struct {
2424
2525// GenerateKeyPair generates a new Ed25519 SSH key pair
2626func GenerateKeyPair (keyName string ) (* KeyPair , error ) {
27- // Generate Ed25519 key pair
2827 publicKey , privateKey , err := ed25519 .GenerateKey (rand .Reader )
2928 if err != nil {
3029 return nil , fmt .Errorf ("failed to generate key pair: %w" , err )
3130 }
3231
33- // Create SSH public key
3432 sshPublicKey , err := ssh .NewPublicKey (publicKey )
3533 if err != nil {
3634 return nil , fmt .Errorf ("failed to create SSH public key: %w" , err )
3735 }
3836
39- // Convert private key to PEM format
4037 privateKeyBytes , err := x509 .MarshalPKCS8PrivateKey (privateKey )
4138 if err != nil {
4239 return nil , fmt .Errorf ("failed to marshal private key: %w" , err )
@@ -47,29 +44,23 @@ func GenerateKeyPair(keyName string) (*KeyPair, error) {
4744 Bytes : privateKeyBytes ,
4845 })
4946
50- // Format SSH public key
5147 sshPublicKeyBytes := ssh .MarshalAuthorizedKey (sshPublicKey )
5248 sshPublicKeyString := strings .TrimSpace (string (sshPublicKeyBytes ))
5349
54- // Get fingerprint
5550 fingerprint := ssh .FingerprintSHA256 (sshPublicKey )
5651
57- // Get lightfold keys directory
5852 keysDir , err := GetKeysDirectory ()
5953 if err != nil {
6054 return nil , fmt .Errorf ("failed to get keys directory: %w" , err )
6155 }
6256
63- // Create key file paths
6457 privateKeyPath := filepath .Join (keysDir , keyName )
6558 publicKeyPath := privateKeyPath + ".pub"
6659
67- // Write private key
6860 if err := os .WriteFile (privateKeyPath , privateKeyPEM , config .PermPrivateKey ); err != nil {
6961 return nil , fmt .Errorf ("failed to write private key: %w" , err )
7062 }
7163
72- // Write public key
7364 if err := os .WriteFile (publicKeyPath , sshPublicKeyBytes , config .PermPublicKey ); err != nil {
7465 return nil , fmt .Errorf ("failed to write public key: %w" , err )
7566 }
@@ -91,7 +82,6 @@ func GetKeysDirectory() (string, error) {
9182
9283 keysDir := filepath .Join (homeDir , config .LocalConfigDir , config .LocalKeysDir )
9384
94- // Create directory if it doesn't exist
9585 if err := os .MkdirAll (keysDir , 0700 ); err != nil {
9686 return "" , fmt .Errorf ("failed to create keys directory: %w" , err )
9787 }
@@ -111,24 +101,19 @@ func LoadPublicKey(publicKeyPath string) (string, error) {
111101
112102// ValidateSSHKey validates that an SSH key file exists and is valid
113103func ValidateSSHKey (keyPath string ) error {
114- // Check if file exists
115104 if _ , err := os .Stat (keyPath ); os .IsNotExist (err ) {
116105 return fmt .Errorf ("SSH key file does not exist: %s" , keyPath )
117106 }
118107
119- // Try to read and parse the key
120108 data , err := os .ReadFile (keyPath )
121109 if err != nil {
122110 return fmt .Errorf ("failed to read SSH key file: %w" , err )
123111 }
124112
125- // Try to parse as private key first
126113 block , _ := pem .Decode (data )
127114 if block != nil {
128- // It's a PEM-encoded private key
129115 _ , err := x509 .ParsePKCS8PrivateKey (block .Bytes )
130116 if err != nil {
131- // Try parsing as RSA private key
132117 _ , err = x509 .ParsePKCS1PrivateKey (block .Bytes )
133118 if err != nil {
134119 return fmt .Errorf ("invalid private key format: %w" , err )
@@ -137,7 +122,6 @@ func ValidateSSHKey(keyPath string) error {
137122 return nil
138123 }
139124
140- // Try to parse as SSH public key
141125 _ , _ , _ , _ , err = ssh .ParseAuthorizedKey (data )
142126 if err != nil {
143127 return fmt .Errorf ("invalid SSH key format: %w" , err )
@@ -148,7 +132,6 @@ func ValidateSSHKey(keyPath string) error {
148132
149133// GetKeyName generates a key name for a project
150134func GetKeyName (projectName string ) string {
151- // Sanitize project name for file system
152135 keyName := strings .ReplaceAll (projectName , " " , "_" )
153136 keyName = strings .ReplaceAll (keyName , "/" , "_" )
154137 keyName = strings .ReplaceAll (keyName , "\\ " , "_" )
@@ -167,7 +150,6 @@ func KeyExists(keyName string) (bool, error) {
167150 privateKeyPath := filepath .Join (keysDir , keyName )
168151 publicKeyPath := privateKeyPath + ".pub"
169152
170- // Check if both files exist
171153 if _ , err := os .Stat (privateKeyPath ); os .IsNotExist (err ) {
172154 return false , nil
173155 }
@@ -177,3 +159,83 @@ func KeyExists(keyName string) (bool, error) {
177159
178160 return true , nil
179161}
162+
163+ // DeleteKeyPair deletes an SSH key pair
164+ func DeleteKeyPair (keyPath string ) error {
165+ if err := os .Remove (keyPath ); err != nil && ! os .IsNotExist (err ) {
166+ return fmt .Errorf ("failed to delete private key: %w" , err )
167+ }
168+
169+ publicKeyPath := keyPath + ".pub"
170+ if err := os .Remove (publicKeyPath ); err != nil && ! os .IsNotExist (err ) {
171+ return fmt .Errorf ("failed to delete public key: %w" , err )
172+ }
173+
174+ return nil
175+ }
176+
177+ // GetSSHKeyFromPath extracts the base key name from a full path
178+ // Example: "/home/user/.lightfold/keys/lightfold_my-project_ed25519" -> "lightfold_my-project_ed25519"
179+ func GetSSHKeyFromPath (keyPath string ) string {
180+ return filepath .Base (keyPath )
181+ }
182+
183+ // CleanupUnusedKeys removes SSH keys that are not used by any targets.
184+ // Returns the number of keys deleted.
185+ func CleanupUnusedKeys (allTargets map [string ]config.TargetConfig ) (int , error ) {
186+ keysDir , err := GetKeysDirectory ()
187+ if err != nil {
188+ return 0 , fmt .Errorf ("failed to get keys directory: %w" , err )
189+ }
190+
191+ keysInUse := make (map [string ]bool )
192+
193+ for _ , target := range allTargets {
194+ providerCfg , err := target .GetSSHProviderConfig ()
195+ if err != nil || providerCfg == nil {
196+ continue
197+ }
198+
199+ sshKey := providerCfg .GetSSHKey ()
200+ if sshKey != "" {
201+ keyName := filepath .Base (sshKey )
202+ keysInUse [keyName ] = true
203+ }
204+ }
205+
206+ entries , err := os .ReadDir (keysDir )
207+ if err != nil {
208+ return 0 , fmt .Errorf ("failed to read keys directory: %w" , err )
209+ }
210+
211+ var keysToDelete []string
212+
213+ for _ , entry := range entries {
214+ if entry .IsDir () {
215+ continue
216+ }
217+
218+ filename := entry .Name ()
219+
220+ // Skip public keys - we'll delete them with their private keys
221+ if strings .HasSuffix (filename , ".pub" ) {
222+ continue
223+ }
224+
225+ if ! strings .HasPrefix (filename , "lightfold_" ) || ! strings .HasSuffix (filename , "_ed25519" ) {
226+ continue
227+ }
228+
229+ if ! keysInUse [filename ] {
230+ keysToDelete = append (keysToDelete , filepath .Join (keysDir , filename ))
231+ }
232+ }
233+
234+ for _ , keyPath := range keysToDelete {
235+ if err := DeleteKeyPair (keyPath ); err != nil {
236+ return len (keysToDelete ), fmt .Errorf ("failed to delete key %s: %w" , filepath .Base (keyPath ), err )
237+ }
238+ }
239+
240+ return len (keysToDelete ), nil
241+ }
0 commit comments