blob: 0054205dc62bcf5004920c9859aff194b4d9533a [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 {
71 if err := fs.Mkdir(base, 0o777); err != nil {
Sean McCullough7d5a6302025-04-24 21:27:51 -070072 return nil, fmt.Errorf("couldn't create %s: %w", base, err)
73 }
74 }
75
Sean McCullough4854c652025-04-24 18:37:02 -070076 cst := &SSHTheater{
77 cntrName: cntrName,
78 sshHost: sshHost,
79 sshPort: sshPort,
80 knownHostsPath: filepath.Join(base, "known_hosts"),
81 userIdentityPath: filepath.Join(base, "container_user_identity"),
82 serverIdentityPath: filepath.Join(base, "container_server_identity"),
83 sshConfigPath: filepath.Join(base, "ssh_config"),
Sean McCullough2cba6952025-04-25 20:32:10 +000084 fs: fs,
85 kg: kg,
Sean McCullough4854c652025-04-24 18:37:02 -070086 }
Sean McCullough2cba6952025-04-25 20:32:10 +000087 if _, err := cst.createKeyPairIfMissing(cst.serverIdentityPath); err != nil {
Sean McCullough7d5a6302025-04-24 21:27:51 -070088 return nil, fmt.Errorf("couldn't create server identity: %w", err)
89 }
Sean McCullough2cba6952025-04-25 20:32:10 +000090 if _, err := cst.createKeyPairIfMissing(cst.userIdentityPath); err != nil {
Sean McCullough7d5a6302025-04-24 21:27:51 -070091 return nil, fmt.Errorf("couldn't create user identity: %w", err)
Sean McCullough4854c652025-04-24 18:37:02 -070092 }
93
Sean McCullough2cba6952025-04-25 20:32:10 +000094 serverIdentity, err := fs.ReadFile(cst.serverIdentityPath)
Sean McCullough4854c652025-04-24 18:37:02 -070095 if err != nil {
96 return nil, fmt.Errorf("couldn't read container's ssh server identity: %w", err)
97 }
98 cst.serverIdentity = serverIdentity
99
Sean McCullough2cba6952025-04-25 20:32:10 +0000100 serverPubKeyBytes, err := fs.ReadFile(cst.serverIdentityPath + ".pub")
101 if err != nil {
102 return nil, fmt.Errorf("couldn't read ssh server public key file: %w", err)
103 }
Sean McCullough4854c652025-04-24 18:37:02 -0700104 serverPubKey, _, _, _, err := ssh.ParseAuthorizedKey(serverPubKeyBytes)
105 if err != nil {
Sean McCullough2cba6952025-04-25 20:32:10 +0000106 return nil, fmt.Errorf("couldn't parse ssh server public key: %w", err)
Sean McCullough4854c652025-04-24 18:37:02 -0700107 }
108 cst.serverPublicKey = serverPubKey
109
Sean McCullough2cba6952025-04-25 20:32:10 +0000110 userIdentity, err := fs.ReadFile(cst.userIdentityPath + ".pub")
Sean McCullough4854c652025-04-24 18:37:02 -0700111 if err != nil {
112 return nil, fmt.Errorf("couldn't read ssh user identity: %w", err)
113 }
114 cst.userIdentity = userIdentity
115
Sean McCullough7d5a6302025-04-24 21:27:51 -0700116 if err := cst.addContainerToSSHConfig(); err != nil {
117 return nil, fmt.Errorf("couldn't add container to ssh_config: %w", err)
118 }
119
120 if err := cst.addContainerToKnownHosts(); err != nil {
121 return nil, fmt.Errorf("couldn't update known hosts: %w", err)
122 }
123
Sean McCullough4854c652025-04-24 18:37:02 -0700124 return cst, nil
125}
126
Sean McCullough2cba6952025-04-25 20:32:10 +0000127func CheckForIncludeWithFS(fs FileSystem) error {
Sean McCullough74b01212025-04-29 18:40:53 -0700128 sketchSSHPathInclude := "Include " + filepath.Join(os.Getenv("HOME"), ".config", "sketch", "ssh_config")
Sean McCulloughf5e28f62025-04-25 10:48:00 -0700129 defaultSSHPath := filepath.Join(os.Getenv("HOME"), ".ssh", "config")
Sean McCullough3b0795b2025-04-29 19:09:23 -0700130 f, _ := fs.OpenFile(filepath.Join(os.Getenv("HOME"), ".ssh", "config"), os.O_RDWR|os.O_CREATE, 0o644)
Sean McCullough2cba6952025-04-25 20:32:10 +0000131 if f == nil {
132 return fmt.Errorf("⚠️ SSH connections are disabled. cannot open SSH config file: %s", defaultSSHPath)
133 }
134 defer f.Close()
Sean McCulloughf5e28f62025-04-25 10:48:00 -0700135 cfg, _ := ssh_config.Decode(f)
136 var sketchInludePos *ssh_config.Position
137 var firstNonIncludePos *ssh_config.Position
138 for _, host := range cfg.Hosts {
139 for _, node := range host.Nodes {
140 inc, ok := node.(*ssh_config.Include)
141 if ok {
142 if strings.TrimSpace(inc.String()) == sketchSSHPathInclude {
143 pos := inc.Pos()
144 sketchInludePos = &pos
145 }
146 } else if firstNonIncludePos == nil && !strings.HasPrefix(strings.TrimSpace(node.String()), "#") {
147 pos := node.Pos()
148 firstNonIncludePos = &pos
149 }
150 }
151 }
152
153 if sketchInludePos == nil {
Sean McCullough3b0795b2025-04-29 19:09:23 -0700154 cfgBytes, err := cfg.MarshalText()
155 if err != nil {
156 return fmt.Errorf("couldn't marshal ssh_config: %w", err)
157 }
158 if err := f.Truncate(0); err != nil {
159 return fmt.Errorf("couldn't truncate ssh_config: %w", err)
160 }
161 if _, err := f.Seek(0, 0); err != nil {
162 return fmt.Errorf("couldn't seek to beginning of ssh_config: %w", err)
163 }
164 cfgBytes = append([]byte(sketchSSHPathInclude+"\n"), cfgBytes...)
165 if _, err := f.Write(cfgBytes); err != nil {
166 return fmt.Errorf("couldn't write ssh_config: %w", err)
167 }
168
169 return nil
Sean McCulloughf5e28f62025-04-25 10:48:00 -0700170 }
171
172 if firstNonIncludePos != nil && firstNonIncludePos.Line < sketchInludePos.Line {
Sean McCullough2cba6952025-04-25 20:32:10 +0000173 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 -0700174 }
175 return nil
176}
177
Sean McCullough4854c652025-04-24 18:37:02 -0700178func removeFromHosts(cntrName string, cfgHosts []*ssh_config.Host) []*ssh_config.Host {
179 hosts := []*ssh_config.Host{}
180 for _, host := range cfgHosts {
181 if host.Matches(cntrName) || strings.Contains(host.String(), cntrName) {
182 continue
183 }
184 patMatch := false
185 for _, pat := range host.Patterns {
186 if strings.Contains(pat.String(), cntrName) {
187 patMatch = true
188 }
189 }
190 if patMatch {
191 continue
192 }
193
194 hosts = append(hosts, host)
195 }
196 return hosts
197}
198
Sean McCullough4854c652025-04-24 18:37:02 -0700199func encodePrivateKeyToPEM(privateKey *rsa.PrivateKey) []byte {
200 pemBlock := &pem.Block{
201 Type: "RSA PRIVATE KEY",
202 Bytes: x509.MarshalPKCS1PrivateKey(privateKey),
203 }
204 pemBytes := pem.EncodeToMemory(pemBlock)
205 return pemBytes
206}
207
Sean McCullough2cba6952025-04-25 20:32:10 +0000208func (c *SSHTheater) writeKeyToFile(keyBytes []byte, filename string) error {
209 err := c.fs.WriteFile(filename, keyBytes, 0o600)
Sean McCullough4854c652025-04-24 18:37:02 -0700210 return err
211}
212
Sean McCullough2cba6952025-04-25 20:32:10 +0000213func (c *SSHTheater) createKeyPairIfMissing(idPath string) (ssh.PublicKey, error) {
214 if _, err := c.fs.Stat(idPath); err == nil {
Sean McCullough4854c652025-04-24 18:37:02 -0700215 return nil, nil
216 }
217
Sean McCullough2cba6952025-04-25 20:32:10 +0000218 privateKey, err := c.kg.GeneratePrivateKey(keyBitSize)
Sean McCullough4854c652025-04-24 18:37:02 -0700219 if err != nil {
Sean McCullough2cba6952025-04-25 20:32:10 +0000220 return nil, fmt.Errorf("error generating private key: %w", err)
Sean McCullough4854c652025-04-24 18:37:02 -0700221 }
222
Sean McCullough2cba6952025-04-25 20:32:10 +0000223 publicRsaKey, err := c.kg.GeneratePublicKey(&privateKey.PublicKey)
Sean McCullough4854c652025-04-24 18:37:02 -0700224 if err != nil {
Sean McCullough2cba6952025-04-25 20:32:10 +0000225 return nil, fmt.Errorf("error generating public key: %w", err)
Sean McCullough4854c652025-04-24 18:37:02 -0700226 }
227
228 privateKeyPEM := encodePrivateKeyToPEM(privateKey)
229
Sean McCullough2cba6952025-04-25 20:32:10 +0000230 err = c.writeKeyToFile(privateKeyPEM, idPath)
Sean McCullough4854c652025-04-24 18:37:02 -0700231 if err != nil {
Sean McCullough2cba6952025-04-25 20:32:10 +0000232 return nil, fmt.Errorf("error writing private key to file %w", err)
Sean McCullough4854c652025-04-24 18:37:02 -0700233 }
234 pubKeyBytes := ssh.MarshalAuthorizedKey(publicRsaKey)
235
Sean McCullough2cba6952025-04-25 20:32:10 +0000236 err = c.writeKeyToFile([]byte(pubKeyBytes), idPath+".pub")
Sean McCullough4854c652025-04-24 18:37:02 -0700237 if err != nil {
Sean McCullough2cba6952025-04-25 20:32:10 +0000238 return nil, fmt.Errorf("error writing public key to file %w", err)
Sean McCullough4854c652025-04-24 18:37:02 -0700239 }
240 return publicRsaKey, nil
241}
242
243func (c *SSHTheater) addSketchHostMatchIfMissing(cfg *ssh_config.Config) error {
244 found := false
245 for _, host := range cfg.Hosts {
246 if strings.Contains(host.String(), "host=\"sketch-*\"") {
247 found = true
248 break
249 }
250 }
251 if !found {
252 hostPattern, err := ssh_config.NewPattern("host=\"sketch-*\"")
253 if err != nil {
254 return fmt.Errorf("couldn't add pattern to ssh_config: %w", err)
255 }
256
257 hostCfg := &ssh_config.Host{Patterns: []*ssh_config.Pattern{hostPattern}}
258 hostCfg.Nodes = append(hostCfg.Nodes, &ssh_config.KV{Key: "UserKnownHostsFile", Value: c.knownHostsPath})
259
Sean McCullough4854c652025-04-24 18:37:02 -0700260 hostCfg.Nodes = append(hostCfg.Nodes, &ssh_config.KV{Key: "IdentityFile", Value: c.userIdentityPath})
Sean McCullough4854c652025-04-24 18:37:02 -0700261 hostCfg.Nodes = append(hostCfg.Nodes, &ssh_config.Empty{})
262
263 cfg.Hosts = append([]*ssh_config.Host{hostCfg}, cfg.Hosts...)
264 }
265 return nil
266}
267
268func (c *SSHTheater) addContainerToSSHConfig() error {
Sean McCullough2cba6952025-04-25 20:32:10 +0000269 f, err := c.fs.OpenFile(c.sshConfigPath, os.O_RDWR|os.O_CREATE, 0o644)
Sean McCullough4854c652025-04-24 18:37:02 -0700270 if err != nil {
271 return fmt.Errorf("couldn't open ssh_config: %w", err)
272 }
273 defer f.Close()
274
275 cfg, err := ssh_config.Decode(f)
276 if err != nil {
277 return fmt.Errorf("couldn't decode ssh_config: %w", err)
278 }
279 cntrPattern, err := ssh_config.NewPattern(c.cntrName)
280 if err != nil {
281 return fmt.Errorf("couldn't add pattern to ssh_config: %w", err)
282 }
283
284 // Remove any matches for this container if they already exist.
285 cfg.Hosts = removeFromHosts(c.cntrName, cfg.Hosts)
286
287 hostCfg := &ssh_config.Host{Patterns: []*ssh_config.Pattern{cntrPattern}}
288 hostCfg.Nodes = append(hostCfg.Nodes, &ssh_config.KV{Key: "HostName", Value: c.sshHost})
289 hostCfg.Nodes = append(hostCfg.Nodes, &ssh_config.KV{Key: "User", Value: "root"})
290 hostCfg.Nodes = append(hostCfg.Nodes, &ssh_config.KV{Key: "Port", Value: c.sshPort})
291 hostCfg.Nodes = append(hostCfg.Nodes, &ssh_config.KV{Key: "IdentityFile", Value: c.userIdentityPath})
292 hostCfg.Nodes = append(hostCfg.Nodes, &ssh_config.KV{Key: "UserKnownHostsFile", Value: c.knownHostsPath})
293
294 hostCfg.Nodes = append(hostCfg.Nodes, &ssh_config.Empty{})
295 cfg.Hosts = append(cfg.Hosts, hostCfg)
296
297 if err := c.addSketchHostMatchIfMissing(cfg); err != nil {
298 return fmt.Errorf("couldn't add missing host match: %w", err)
299 }
300
301 cfgBytes, err := cfg.MarshalText()
302 if err != nil {
303 return fmt.Errorf("couldn't marshal ssh_config: %w", err)
304 }
305 if err := f.Truncate(0); err != nil {
306 return fmt.Errorf("couldn't truncate ssh_config: %w", err)
307 }
308 if _, err := f.Seek(0, 0); err != nil {
309 return fmt.Errorf("couldn't seek to beginning of ssh_config: %w", err)
310 }
311 if _, err := f.Write(cfgBytes); err != nil {
312 return fmt.Errorf("couldn't write ssh_config: %w", err)
313 }
314
315 return nil
316}
317
318func (c *SSHTheater) addContainerToKnownHosts() error {
Sean McCullough2cba6952025-04-25 20:32:10 +0000319 f, err := c.fs.OpenFile(c.knownHostsPath, os.O_RDWR|os.O_CREATE, 0o644)
Sean McCullough4854c652025-04-24 18:37:02 -0700320 if err != nil {
321 return fmt.Errorf("couldn't open %s: %w", c.knownHostsPath, err)
322 }
323 defer f.Close()
Sean McCullough7d5a6302025-04-24 21:27:51 -0700324 pkBytes := c.serverPublicKey.Marshal()
325 if len(pkBytes) == 0 {
Sean McCullough2cba6952025-04-25 20:32:10 +0000326 return fmt.Errorf("empty serverPublicKey, this is a bug")
Sean McCullough7d5a6302025-04-24 21:27:51 -0700327 }
328 newHostLine := knownhosts.Line([]string{c.sshHost + ":" + c.sshPort}, c.serverPublicKey)
Sean McCullough4854c652025-04-24 18:37:02 -0700329
Sean McCullough7d5a6302025-04-24 21:27:51 -0700330 outputLines := []string{}
331 scanner := bufio.NewScanner(f)
332 for scanner.Scan() {
333 outputLines = append(outputLines, scanner.Text())
334 }
335 outputLines = append(outputLines, newHostLine)
336 if err := f.Truncate(0); err != nil {
337 return fmt.Errorf("couldn't truncate known_hosts: %w", err)
338 }
339 if _, err := f.Seek(0, 0); err != nil {
340 return fmt.Errorf("couldn't seek to beginning of known_hosts: %w", err)
341 }
342 if _, err := f.Write([]byte(strings.Join(outputLines, "\n"))); err != nil {
343 return fmt.Errorf("couldn't write updated known_hosts to to %s: %w", c.knownHostsPath, err)
Sean McCullough4854c652025-04-24 18:37:02 -0700344 }
345
346 return nil
347}
348
349func (c *SSHTheater) removeContainerFromKnownHosts() error {
Sean McCullough2cba6952025-04-25 20:32:10 +0000350 f, err := c.fs.OpenFile(c.knownHostsPath, os.O_RDWR|os.O_CREATE, 0o644)
Sean McCullough4854c652025-04-24 18:37:02 -0700351 if err != nil {
352 return fmt.Errorf("couldn't open ssh_config: %w", err)
353 }
354 defer f.Close()
355 scanner := bufio.NewScanner(f)
Sean McCullough7d5a6302025-04-24 21:27:51 -0700356 lineToRemove := knownhosts.Line([]string{c.sshHost + ":" + c.sshPort}, c.serverPublicKey)
Sean McCullough4854c652025-04-24 18:37:02 -0700357 outputLines := []string{}
358 for scanner.Scan() {
359 if scanner.Text() == lineToRemove {
360 continue
361 }
362 outputLines = append(outputLines, scanner.Text())
363 }
364 if err := f.Truncate(0); err != nil {
365 return fmt.Errorf("couldn't truncate known_hosts: %w", err)
366 }
367 if _, err := f.Seek(0, 0); err != nil {
368 return fmt.Errorf("couldn't seek to beginning of known_hosts: %w", err)
369 }
370 if _, err := f.Write([]byte(strings.Join(outputLines, "\n"))); err != nil {
371 return fmt.Errorf("couldn't write updated known_hosts to to %s: %w", c.knownHostsPath, err)
372 }
373
374 return nil
375}
376
377func (c *SSHTheater) Cleanup() error {
378 if err := c.removeContainerFromSSHConfig(); err != nil {
379 return fmt.Errorf("couldn't remove container from ssh_config: %v\n", err)
380 }
381 if err := c.removeContainerFromKnownHosts(); err != nil {
382 return fmt.Errorf("couldn't remove container from ssh_config: %v\n", err)
383 }
384
385 return nil
386}
387
388func (c *SSHTheater) removeContainerFromSSHConfig() error {
Sean McCullough2cba6952025-04-25 20:32:10 +0000389 f, err := c.fs.OpenFile(c.sshConfigPath, os.O_RDWR|os.O_CREATE, 0o644)
Sean McCullough4854c652025-04-24 18:37:02 -0700390 if err != nil {
391 return fmt.Errorf("couldn't open ssh_config: %w", err)
392 }
393 defer f.Close()
394
395 cfg, err := ssh_config.Decode(f)
396 if err != nil {
397 return fmt.Errorf("couldn't decode ssh_config: %w", err)
398 }
399 cfg.Hosts = removeFromHosts(c.cntrName, cfg.Hosts)
400
401 if err := c.addSketchHostMatchIfMissing(cfg); err != nil {
402 return fmt.Errorf("couldn't add missing host match: %w", err)
403 }
404
405 cfgBytes, err := cfg.MarshalText()
406 if err != nil {
407 return fmt.Errorf("couldn't marshal ssh_config: %w", err)
408 }
409 if err := f.Truncate(0); err != nil {
410 return fmt.Errorf("couldn't truncate ssh_config: %w", err)
411 }
412 if _, err := f.Seek(0, 0); err != nil {
413 return fmt.Errorf("couldn't seek to beginning of ssh_config: %w", err)
414 }
415 if _, err := f.Write(cfgBytes); err != nil {
416 return fmt.Errorf("couldn't write ssh_config: %w", err)
417 }
418 return nil
419}
Sean McCullough2cba6952025-04-25 20:32:10 +0000420
421// FileSystem represents a filesystem interface for testability
422type FileSystem interface {
423 Stat(name string) (fs.FileInfo, error)
424 Mkdir(name string, perm fs.FileMode) error
425 ReadFile(name string) ([]byte, error)
426 WriteFile(name string, data []byte, perm fs.FileMode) error
427 OpenFile(name string, flag int, perm fs.FileMode) (*os.File, error)
428}
429
430// RealFileSystem is the default implementation of FileSystem that uses the OS
431type RealFileSystem struct{}
432
433func (fs *RealFileSystem) Stat(name string) (fs.FileInfo, error) {
434 return os.Stat(name)
435}
436
437func (fs *RealFileSystem) Mkdir(name string, perm fs.FileMode) error {
438 return os.Mkdir(name, perm)
439}
440
441func (fs *RealFileSystem) ReadFile(name string) ([]byte, error) {
442 return os.ReadFile(name)
443}
444
445func (fs *RealFileSystem) WriteFile(name string, data []byte, perm fs.FileMode) error {
446 return os.WriteFile(name, data, perm)
447}
448
449func (fs *RealFileSystem) OpenFile(name string, flag int, perm fs.FileMode) (*os.File, error) {
450 return os.OpenFile(name, flag, perm)
451}
452
453// KeyGenerator represents an interface for generating SSH keys for testability
454type KeyGenerator interface {
455 GeneratePrivateKey(bitSize int) (*rsa.PrivateKey, error)
456 GeneratePublicKey(privateKey *rsa.PublicKey) (ssh.PublicKey, error)
457}
458
459// RealKeyGenerator is the default implementation of KeyGenerator
460type RealKeyGenerator struct{}
461
462func (kg *RealKeyGenerator) GeneratePrivateKey(bitSize int) (*rsa.PrivateKey, error) {
463 return rsa.GenerateKey(rand.Reader, bitSize)
464}
465
466func (kg *RealKeyGenerator) GeneratePublicKey(privateKey *rsa.PublicKey) (ssh.PublicKey, error) {
467 return ssh.NewPublicKey(privateKey)
468}
469
470// CheckForInclude checks if the user's SSH config includes the Sketch SSH config file
471func CheckForInclude() error {
472 return CheckForIncludeWithFS(&RealFileSystem{})
473}