blob: 8a2e94b225553a00a26153dbc15fb438f360021c [file] [log] [blame]
Sean McCullough4854c652025-04-24 18:37:02 -07001package dockerimg
2
3import (
4 "bufio"
5 "crypto/rand"
6 "crypto/rsa"
7 "crypto/x509"
Sean McCullough4854c652025-04-24 18:37:02 -07008 "encoding/pem"
9 "fmt"
Sean McCullough2cba6952025-04-25 20:32:10 +000010 "io/fs"
Sean McCullough4854c652025-04-24 18:37:02 -070011 "os"
12 "path/filepath"
13 "strings"
14
15 "github.com/kevinburke/ssh_config"
16 "golang.org/x/crypto/ssh"
Sean McCullough7d5a6302025-04-24 21:27:51 -070017 "golang.org/x/crypto/ssh/knownhosts"
Sean McCullough4854c652025-04-24 18:37:02 -070018)
19
20const keyBitSize = 2048
21
22// SSHTheater does the necessary key pair generation, known_hosts updates, ssh_config file updates etc steps
23// so that ssh can connect to a locally running sketch container to other local processes like vscode without
24// the user having to run the usual ssh obstacle course.
25//
26// SSHTheater does not modify your default .ssh/config, or known_hosts files. However, in order for you
27// to be able to use it properly you will have to make a one-time edit to your ~/.ssh/config file.
28//
29// In your ~/.ssh/config file, add the following line:
30//
Sean McCullough74b01212025-04-29 18:40:53 -070031// Include $HOME/.config/sketch/ssh_config
Sean McCullough4854c652025-04-24 18:37:02 -070032//
33// where $HOME is your home directory.
34type SSHTheater struct {
35 cntrName string
36 sshHost string
37 sshPort string
38
39 knownHostsPath string
40 userIdentityPath string
41 sshConfigPath string
42 serverIdentityPath string
43
44 serverPublicKey ssh.PublicKey
45 serverIdentity []byte
46 userIdentity []byte
Sean McCullough2cba6952025-04-25 20:32:10 +000047
48 fs FileSystem
49 kg KeyGenerator
Sean McCullough4854c652025-04-24 18:37:02 -070050}
51
52// NewSSHTheather will set up everything so that you can use ssh on localhost to connect to
53// the sketch container. Call #Clean when you are done with the container to remove the
54// various entries it created in its known_hosts and ssh_config files. Also note that
55// this will generate key pairs for both the ssh server identity and the user identity, if
56// these files do not already exist. These key pair files are not deleted by #Cleanup,
57// so they can be re-used across invocations of sketch. This means every sketch container
58// that runs on this host will use the same ssh server identity.
59//
60// If this doesn't return an error, you should be able to run "ssh <cntrName>"
61// in a terminal on your host machine to open a shell into the container without having
62// to manually accept changes to your known_hosts file etc.
63func NewSSHTheather(cntrName, sshHost, sshPort string) (*SSHTheater, error) {
Sean McCullough2cba6952025-04-25 20:32:10 +000064 return newSSHTheatherWithDeps(cntrName, sshHost, sshPort, &RealFileSystem{}, &RealKeyGenerator{})
65}
66
67// newSSHTheatherWithDeps creates a new SSHTheater with the specified dependencies
68func newSSHTheatherWithDeps(cntrName, sshHost, sshPort string, fs FileSystem, kg KeyGenerator) (*SSHTheater, error) {
Sean McCullough74b01212025-04-29 18:40:53 -070069 base := filepath.Join(os.Getenv("HOME"), ".config", "sketch")
Sean McCullough2cba6952025-04-25 20:32:10 +000070 if _, err := fs.Stat(base); err != nil {
Sean McCulloughc796e7f2025-04-30 08:44:06 -070071
72 if err := fs.MkdirAll(base, 0o777); err != nil {
Sean McCullough7d5a6302025-04-24 21:27:51 -070073 return nil, fmt.Errorf("couldn't create %s: %w", base, err)
74 }
75 }
76
Sean McCullough4854c652025-04-24 18:37:02 -070077 cst := &SSHTheater{
78 cntrName: cntrName,
79 sshHost: sshHost,
80 sshPort: sshPort,
81 knownHostsPath: filepath.Join(base, "known_hosts"),
82 userIdentityPath: filepath.Join(base, "container_user_identity"),
83 serverIdentityPath: filepath.Join(base, "container_server_identity"),
84 sshConfigPath: filepath.Join(base, "ssh_config"),
Sean McCullough2cba6952025-04-25 20:32:10 +000085 fs: fs,
86 kg: kg,
Sean McCullough4854c652025-04-24 18:37:02 -070087 }
Sean McCullough2cba6952025-04-25 20:32:10 +000088 if _, err := cst.createKeyPairIfMissing(cst.serverIdentityPath); err != nil {
Sean McCullough7d5a6302025-04-24 21:27:51 -070089 return nil, fmt.Errorf("couldn't create server identity: %w", err)
90 }
Sean McCullough2cba6952025-04-25 20:32:10 +000091 if _, err := cst.createKeyPairIfMissing(cst.userIdentityPath); err != nil {
Sean McCullough7d5a6302025-04-24 21:27:51 -070092 return nil, fmt.Errorf("couldn't create user identity: %w", err)
Sean McCullough4854c652025-04-24 18:37:02 -070093 }
94
Sean McCullough2cba6952025-04-25 20:32:10 +000095 serverIdentity, err := fs.ReadFile(cst.serverIdentityPath)
Sean McCullough4854c652025-04-24 18:37:02 -070096 if err != nil {
97 return nil, fmt.Errorf("couldn't read container's ssh server identity: %w", err)
98 }
99 cst.serverIdentity = serverIdentity
100
Sean McCullough2cba6952025-04-25 20:32:10 +0000101 serverPubKeyBytes, err := fs.ReadFile(cst.serverIdentityPath + ".pub")
102 if err != nil {
103 return nil, fmt.Errorf("couldn't read ssh server public key file: %w", err)
104 }
Sean McCullough4854c652025-04-24 18:37:02 -0700105 serverPubKey, _, _, _, err := ssh.ParseAuthorizedKey(serverPubKeyBytes)
106 if err != nil {
Sean McCullough2cba6952025-04-25 20:32:10 +0000107 return nil, fmt.Errorf("couldn't parse ssh server public key: %w", err)
Sean McCullough4854c652025-04-24 18:37:02 -0700108 }
109 cst.serverPublicKey = serverPubKey
110
Sean McCullough2cba6952025-04-25 20:32:10 +0000111 userIdentity, err := fs.ReadFile(cst.userIdentityPath + ".pub")
Sean McCullough4854c652025-04-24 18:37:02 -0700112 if err != nil {
113 return nil, fmt.Errorf("couldn't read ssh user identity: %w", err)
114 }
115 cst.userIdentity = userIdentity
116
Sean McCullough7d5a6302025-04-24 21:27:51 -0700117 if err := cst.addContainerToSSHConfig(); err != nil {
118 return nil, fmt.Errorf("couldn't add container to ssh_config: %w", err)
119 }
120
121 if err := cst.addContainerToKnownHosts(); err != nil {
122 return nil, fmt.Errorf("couldn't update known hosts: %w", err)
123 }
124
Sean McCullough4854c652025-04-24 18:37:02 -0700125 return cst, nil
126}
127
Sean McCullough2cba6952025-04-25 20:32:10 +0000128func CheckForIncludeWithFS(fs FileSystem) error {
Sean McCullough74b01212025-04-29 18:40:53 -0700129 sketchSSHPathInclude := "Include " + filepath.Join(os.Getenv("HOME"), ".config", "sketch", "ssh_config")
Sean McCulloughf5e28f62025-04-25 10:48:00 -0700130 defaultSSHPath := filepath.Join(os.Getenv("HOME"), ".ssh", "config")
Sean McCullough3b0795b2025-04-29 19:09:23 -0700131 f, _ := fs.OpenFile(filepath.Join(os.Getenv("HOME"), ".ssh", "config"), os.O_RDWR|os.O_CREATE, 0o644)
Sean McCullough2cba6952025-04-25 20:32:10 +0000132 if f == nil {
133 return fmt.Errorf("⚠️ SSH connections are disabled. cannot open SSH config file: %s", defaultSSHPath)
134 }
135 defer f.Close()
Sean McCulloughf5e28f62025-04-25 10:48:00 -0700136 cfg, _ := ssh_config.Decode(f)
137 var sketchInludePos *ssh_config.Position
138 var firstNonIncludePos *ssh_config.Position
139 for _, host := range cfg.Hosts {
140 for _, node := range host.Nodes {
141 inc, ok := node.(*ssh_config.Include)
142 if ok {
143 if strings.TrimSpace(inc.String()) == sketchSSHPathInclude {
144 pos := inc.Pos()
145 sketchInludePos = &pos
146 }
147 } else if firstNonIncludePos == nil && !strings.HasPrefix(strings.TrimSpace(node.String()), "#") {
148 pos := node.Pos()
149 firstNonIncludePos = &pos
150 }
151 }
152 }
153
154 if sketchInludePos == nil {
Sean McCullough3b0795b2025-04-29 19:09:23 -0700155 cfgBytes, err := cfg.MarshalText()
156 if err != nil {
157 return fmt.Errorf("couldn't marshal ssh_config: %w", err)
158 }
159 if err := f.Truncate(0); err != nil {
160 return fmt.Errorf("couldn't truncate ssh_config: %w", err)
161 }
162 if _, err := f.Seek(0, 0); err != nil {
163 return fmt.Errorf("couldn't seek to beginning of ssh_config: %w", err)
164 }
165 cfgBytes = append([]byte(sketchSSHPathInclude+"\n"), cfgBytes...)
166 if _, err := f.Write(cfgBytes); err != nil {
167 return fmt.Errorf("couldn't write ssh_config: %w", err)
168 }
169
170 return nil
Sean McCulloughf5e28f62025-04-25 10:48:00 -0700171 }
172
173 if firstNonIncludePos != nil && firstNonIncludePos.Line < sketchInludePos.Line {
Sean McCullough2cba6952025-04-25 20:32:10 +0000174 fmt.Printf("⚠️ SSH confg warning: the location of the Include statement for sketch's ssh config on line %d of %s may prevent ssh from working with sketch containers. try moving it to the top of the file (before any 'Host' lines) if ssh isn't working for you.\n", sketchInludePos.Line, defaultSSHPath)
Sean McCulloughf5e28f62025-04-25 10:48:00 -0700175 }
176 return nil
177}
178
Sean McCullough4854c652025-04-24 18:37:02 -0700179func removeFromHosts(cntrName string, cfgHosts []*ssh_config.Host) []*ssh_config.Host {
180 hosts := []*ssh_config.Host{}
181 for _, host := range cfgHosts {
182 if host.Matches(cntrName) || strings.Contains(host.String(), cntrName) {
183 continue
184 }
185 patMatch := false
186 for _, pat := range host.Patterns {
187 if strings.Contains(pat.String(), cntrName) {
188 patMatch = true
189 }
190 }
191 if patMatch {
192 continue
193 }
194
195 hosts = append(hosts, host)
196 }
197 return hosts
198}
199
Sean McCullough4854c652025-04-24 18:37:02 -0700200func encodePrivateKeyToPEM(privateKey *rsa.PrivateKey) []byte {
201 pemBlock := &pem.Block{
202 Type: "RSA PRIVATE KEY",
203 Bytes: x509.MarshalPKCS1PrivateKey(privateKey),
204 }
205 pemBytes := pem.EncodeToMemory(pemBlock)
206 return pemBytes
207}
208
Sean McCullough2cba6952025-04-25 20:32:10 +0000209func (c *SSHTheater) writeKeyToFile(keyBytes []byte, filename string) error {
210 err := c.fs.WriteFile(filename, keyBytes, 0o600)
Sean McCullough4854c652025-04-24 18:37:02 -0700211 return err
212}
213
Sean McCullough2cba6952025-04-25 20:32:10 +0000214func (c *SSHTheater) createKeyPairIfMissing(idPath string) (ssh.PublicKey, error) {
215 if _, err := c.fs.Stat(idPath); err == nil {
Sean McCullough4854c652025-04-24 18:37:02 -0700216 return nil, nil
217 }
218
Sean McCullough2cba6952025-04-25 20:32:10 +0000219 privateKey, err := c.kg.GeneratePrivateKey(keyBitSize)
Sean McCullough4854c652025-04-24 18:37:02 -0700220 if err != nil {
Sean McCullough2cba6952025-04-25 20:32:10 +0000221 return nil, fmt.Errorf("error generating private key: %w", err)
Sean McCullough4854c652025-04-24 18:37:02 -0700222 }
223
Sean McCullough2cba6952025-04-25 20:32:10 +0000224 publicRsaKey, err := c.kg.GeneratePublicKey(&privateKey.PublicKey)
Sean McCullough4854c652025-04-24 18:37:02 -0700225 if err != nil {
Sean McCullough2cba6952025-04-25 20:32:10 +0000226 return nil, fmt.Errorf("error generating public key: %w", err)
Sean McCullough4854c652025-04-24 18:37:02 -0700227 }
228
229 privateKeyPEM := encodePrivateKeyToPEM(privateKey)
230
Sean McCullough2cba6952025-04-25 20:32:10 +0000231 err = c.writeKeyToFile(privateKeyPEM, idPath)
Sean McCullough4854c652025-04-24 18:37:02 -0700232 if err != nil {
Sean McCullough2cba6952025-04-25 20:32:10 +0000233 return nil, fmt.Errorf("error writing private key to file %w", err)
Sean McCullough4854c652025-04-24 18:37:02 -0700234 }
235 pubKeyBytes := ssh.MarshalAuthorizedKey(publicRsaKey)
236
Sean McCullough2cba6952025-04-25 20:32:10 +0000237 err = c.writeKeyToFile([]byte(pubKeyBytes), idPath+".pub")
Sean McCullough4854c652025-04-24 18:37:02 -0700238 if err != nil {
Sean McCullough2cba6952025-04-25 20:32:10 +0000239 return nil, fmt.Errorf("error writing public key to file %w", err)
Sean McCullough4854c652025-04-24 18:37:02 -0700240 }
241 return publicRsaKey, nil
242}
243
244func (c *SSHTheater) addSketchHostMatchIfMissing(cfg *ssh_config.Config) error {
245 found := false
246 for _, host := range cfg.Hosts {
247 if strings.Contains(host.String(), "host=\"sketch-*\"") {
248 found = true
249 break
250 }
251 }
252 if !found {
253 hostPattern, err := ssh_config.NewPattern("host=\"sketch-*\"")
254 if err != nil {
255 return fmt.Errorf("couldn't add pattern to ssh_config: %w", err)
256 }
257
258 hostCfg := &ssh_config.Host{Patterns: []*ssh_config.Pattern{hostPattern}}
259 hostCfg.Nodes = append(hostCfg.Nodes, &ssh_config.KV{Key: "UserKnownHostsFile", Value: c.knownHostsPath})
260
Sean McCullough4854c652025-04-24 18:37:02 -0700261 hostCfg.Nodes = append(hostCfg.Nodes, &ssh_config.KV{Key: "IdentityFile", Value: c.userIdentityPath})
Sean McCullough4854c652025-04-24 18:37:02 -0700262 hostCfg.Nodes = append(hostCfg.Nodes, &ssh_config.Empty{})
263
264 cfg.Hosts = append([]*ssh_config.Host{hostCfg}, cfg.Hosts...)
265 }
266 return nil
267}
268
269func (c *SSHTheater) addContainerToSSHConfig() error {
Sean McCullough2cba6952025-04-25 20:32:10 +0000270 f, err := c.fs.OpenFile(c.sshConfigPath, os.O_RDWR|os.O_CREATE, 0o644)
Sean McCullough4854c652025-04-24 18:37:02 -0700271 if err != nil {
272 return fmt.Errorf("couldn't open ssh_config: %w", err)
273 }
274 defer f.Close()
275
276 cfg, err := ssh_config.Decode(f)
277 if err != nil {
278 return fmt.Errorf("couldn't decode ssh_config: %w", err)
279 }
280 cntrPattern, err := ssh_config.NewPattern(c.cntrName)
281 if err != nil {
282 return fmt.Errorf("couldn't add pattern to ssh_config: %w", err)
283 }
284
285 // Remove any matches for this container if they already exist.
286 cfg.Hosts = removeFromHosts(c.cntrName, cfg.Hosts)
287
288 hostCfg := &ssh_config.Host{Patterns: []*ssh_config.Pattern{cntrPattern}}
289 hostCfg.Nodes = append(hostCfg.Nodes, &ssh_config.KV{Key: "HostName", Value: c.sshHost})
290 hostCfg.Nodes = append(hostCfg.Nodes, &ssh_config.KV{Key: "User", Value: "root"})
291 hostCfg.Nodes = append(hostCfg.Nodes, &ssh_config.KV{Key: "Port", Value: c.sshPort})
292 hostCfg.Nodes = append(hostCfg.Nodes, &ssh_config.KV{Key: "IdentityFile", Value: c.userIdentityPath})
293 hostCfg.Nodes = append(hostCfg.Nodes, &ssh_config.KV{Key: "UserKnownHostsFile", Value: c.knownHostsPath})
294
295 hostCfg.Nodes = append(hostCfg.Nodes, &ssh_config.Empty{})
296 cfg.Hosts = append(cfg.Hosts, hostCfg)
297
298 if err := c.addSketchHostMatchIfMissing(cfg); err != nil {
299 return fmt.Errorf("couldn't add missing host match: %w", err)
300 }
301
302 cfgBytes, err := cfg.MarshalText()
303 if err != nil {
304 return fmt.Errorf("couldn't marshal ssh_config: %w", err)
305 }
306 if err := f.Truncate(0); err != nil {
307 return fmt.Errorf("couldn't truncate ssh_config: %w", err)
308 }
309 if _, err := f.Seek(0, 0); err != nil {
310 return fmt.Errorf("couldn't seek to beginning of ssh_config: %w", err)
311 }
312 if _, err := f.Write(cfgBytes); err != nil {
313 return fmt.Errorf("couldn't write ssh_config: %w", err)
314 }
315
316 return nil
317}
318
319func (c *SSHTheater) addContainerToKnownHosts() error {
Sean McCullough2cba6952025-04-25 20:32:10 +0000320 f, err := c.fs.OpenFile(c.knownHostsPath, os.O_RDWR|os.O_CREATE, 0o644)
Sean McCullough4854c652025-04-24 18:37:02 -0700321 if err != nil {
322 return fmt.Errorf("couldn't open %s: %w", c.knownHostsPath, err)
323 }
324 defer f.Close()
Sean McCullough7d5a6302025-04-24 21:27:51 -0700325 pkBytes := c.serverPublicKey.Marshal()
326 if len(pkBytes) == 0 {
Sean McCullough2cba6952025-04-25 20:32:10 +0000327 return fmt.Errorf("empty serverPublicKey, this is a bug")
Sean McCullough7d5a6302025-04-24 21:27:51 -0700328 }
329 newHostLine := knownhosts.Line([]string{c.sshHost + ":" + c.sshPort}, c.serverPublicKey)
Sean McCullough4854c652025-04-24 18:37:02 -0700330
Sean McCullough7d5a6302025-04-24 21:27:51 -0700331 outputLines := []string{}
332 scanner := bufio.NewScanner(f)
333 for scanner.Scan() {
334 outputLines = append(outputLines, scanner.Text())
335 }
336 outputLines = append(outputLines, newHostLine)
337 if err := f.Truncate(0); err != nil {
338 return fmt.Errorf("couldn't truncate known_hosts: %w", err)
339 }
340 if _, err := f.Seek(0, 0); err != nil {
341 return fmt.Errorf("couldn't seek to beginning of known_hosts: %w", err)
342 }
343 if _, err := f.Write([]byte(strings.Join(outputLines, "\n"))); err != nil {
344 return fmt.Errorf("couldn't write updated known_hosts to to %s: %w", c.knownHostsPath, err)
Sean McCullough4854c652025-04-24 18:37:02 -0700345 }
346
347 return nil
348}
349
350func (c *SSHTheater) removeContainerFromKnownHosts() error {
Sean McCullough2cba6952025-04-25 20:32:10 +0000351 f, err := c.fs.OpenFile(c.knownHostsPath, os.O_RDWR|os.O_CREATE, 0o644)
Sean McCullough4854c652025-04-24 18:37:02 -0700352 if err != nil {
353 return fmt.Errorf("couldn't open ssh_config: %w", err)
354 }
355 defer f.Close()
356 scanner := bufio.NewScanner(f)
Sean McCullough7d5a6302025-04-24 21:27:51 -0700357 lineToRemove := knownhosts.Line([]string{c.sshHost + ":" + c.sshPort}, c.serverPublicKey)
Sean McCullough4854c652025-04-24 18:37:02 -0700358 outputLines := []string{}
359 for scanner.Scan() {
360 if scanner.Text() == lineToRemove {
361 continue
362 }
363 outputLines = append(outputLines, scanner.Text())
364 }
365 if err := f.Truncate(0); err != nil {
366 return fmt.Errorf("couldn't truncate known_hosts: %w", err)
367 }
368 if _, err := f.Seek(0, 0); err != nil {
369 return fmt.Errorf("couldn't seek to beginning of known_hosts: %w", err)
370 }
371 if _, err := f.Write([]byte(strings.Join(outputLines, "\n"))); err != nil {
372 return fmt.Errorf("couldn't write updated known_hosts to to %s: %w", c.knownHostsPath, err)
373 }
374
375 return nil
376}
377
378func (c *SSHTheater) Cleanup() error {
379 if err := c.removeContainerFromSSHConfig(); err != nil {
380 return fmt.Errorf("couldn't remove container from ssh_config: %v\n", err)
381 }
382 if err := c.removeContainerFromKnownHosts(); err != nil {
383 return fmt.Errorf("couldn't remove container from ssh_config: %v\n", err)
384 }
385
386 return nil
387}
388
389func (c *SSHTheater) removeContainerFromSSHConfig() error {
Sean McCullough2cba6952025-04-25 20:32:10 +0000390 f, err := c.fs.OpenFile(c.sshConfigPath, os.O_RDWR|os.O_CREATE, 0o644)
Sean McCullough4854c652025-04-24 18:37:02 -0700391 if err != nil {
392 return fmt.Errorf("couldn't open ssh_config: %w", err)
393 }
394 defer f.Close()
395
396 cfg, err := ssh_config.Decode(f)
397 if err != nil {
398 return fmt.Errorf("couldn't decode ssh_config: %w", err)
399 }
400 cfg.Hosts = removeFromHosts(c.cntrName, cfg.Hosts)
401
402 if err := c.addSketchHostMatchIfMissing(cfg); err != nil {
403 return fmt.Errorf("couldn't add missing host match: %w", err)
404 }
405
406 cfgBytes, err := cfg.MarshalText()
407 if err != nil {
408 return fmt.Errorf("couldn't marshal ssh_config: %w", err)
409 }
410 if err := f.Truncate(0); err != nil {
411 return fmt.Errorf("couldn't truncate ssh_config: %w", err)
412 }
413 if _, err := f.Seek(0, 0); err != nil {
414 return fmt.Errorf("couldn't seek to beginning of ssh_config: %w", err)
415 }
416 if _, err := f.Write(cfgBytes); err != nil {
417 return fmt.Errorf("couldn't write ssh_config: %w", err)
418 }
419 return nil
420}
Sean McCullough2cba6952025-04-25 20:32:10 +0000421
422// FileSystem represents a filesystem interface for testability
423type FileSystem interface {
424 Stat(name string) (fs.FileInfo, error)
425 Mkdir(name string, perm fs.FileMode) error
Sean McCulloughc796e7f2025-04-30 08:44:06 -0700426 MkdirAll(name string, perm fs.FileMode) error
Sean McCullough2cba6952025-04-25 20:32:10 +0000427 ReadFile(name string) ([]byte, error)
428 WriteFile(name string, data []byte, perm fs.FileMode) error
429 OpenFile(name string, flag int, perm fs.FileMode) (*os.File, error)
430}
431
Sean McCulloughc796e7f2025-04-30 08:44:06 -0700432func (fs *RealFileSystem) MkdirAll(name string, perm fs.FileMode) error {
433 return os.MkdirAll(name, perm)
434}
435
Sean McCullough2cba6952025-04-25 20:32:10 +0000436// RealFileSystem is the default implementation of FileSystem that uses the OS
437type RealFileSystem struct{}
438
439func (fs *RealFileSystem) Stat(name string) (fs.FileInfo, error) {
440 return os.Stat(name)
441}
442
443func (fs *RealFileSystem) Mkdir(name string, perm fs.FileMode) error {
444 return os.Mkdir(name, perm)
445}
446
447func (fs *RealFileSystem) ReadFile(name string) ([]byte, error) {
448 return os.ReadFile(name)
449}
450
451func (fs *RealFileSystem) WriteFile(name string, data []byte, perm fs.FileMode) error {
452 return os.WriteFile(name, data, perm)
453}
454
455func (fs *RealFileSystem) OpenFile(name string, flag int, perm fs.FileMode) (*os.File, error) {
456 return os.OpenFile(name, flag, perm)
457}
458
459// KeyGenerator represents an interface for generating SSH keys for testability
460type KeyGenerator interface {
461 GeneratePrivateKey(bitSize int) (*rsa.PrivateKey, error)
462 GeneratePublicKey(privateKey *rsa.PublicKey) (ssh.PublicKey, error)
463}
464
465// RealKeyGenerator is the default implementation of KeyGenerator
466type RealKeyGenerator struct{}
467
468func (kg *RealKeyGenerator) GeneratePrivateKey(bitSize int) (*rsa.PrivateKey, error) {
469 return rsa.GenerateKey(rand.Reader, bitSize)
470}
471
472func (kg *RealKeyGenerator) GeneratePublicKey(privateKey *rsa.PublicKey) (ssh.PublicKey, error) {
473 return ssh.NewPublicKey(privateKey)
474}
475
476// CheckForInclude checks if the user's SSH config includes the Sketch SSH config file
477func CheckForInclude() error {
478 return CheckForIncludeWithFS(&RealFileSystem{})
479}