github.com/yourbase/yb@v0.7.1/cmd/yb/keychain.go (about)

     1  // Copyright 2021 YourBase Inc.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //		 https://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  //
    15  // SPDX-License-Identifier: Apache-2.0
    16  
    17  package main
    18  
    19  import (
    20  	"context"
    21  	"fmt"
    22  	"path/filepath"
    23  	"strings"
    24  
    25  	"github.com/yourbase/yb/internal/biome"
    26  	"zombiezen.com/go/log"
    27  )
    28  
    29  // ensureKeychain ensures that a default keychain is present in the biome.
    30  // If the biome is not a macOS environment, then ensureKeychain does nothing.
    31  func ensureKeychain(ctx context.Context, bio biome.Biome) error {
    32  	if bio.Describe().OS != biome.MacOS {
    33  		return nil
    34  	}
    35  
    36  	// Check whether a default keychain already exists.
    37  	stdout := new(strings.Builder)
    38  	stderr := new(strings.Builder)
    39  	err := bio.Run(ctx, &biome.Invocation{
    40  		Argv:   []string{"security", "default-keychain", "-d", "user"},
    41  		Stdout: stdout,
    42  		Stderr: stderr,
    43  	})
    44  	if err == nil && len(parseKeychainOutput(stdout.String())) > 0 {
    45  		// Keychain already exists.
    46  		return nil
    47  	}
    48  	if err != nil {
    49  		if stderr.Len() > 0 {
    50  			log.Debugf(ctx, "No default keychain; will create. Error:\n%s%v", stderr, err)
    51  		} else {
    52  			log.Debugf(ctx, "No default keychain; will create. Error: %v", err)
    53  		}
    54  	}
    55  
    56  	// From experimentation, `security list-keychains -s` will silently fail
    57  	// unless ~/Library/Preferences exists.
    58  	if err := biome.MkdirAll(ctx, bio, bio.JoinPath(bio.Dirs().Home, "Library", "Preferences")); err != nil {
    59  		return fmt.Errorf("ensure build environment keychain: %w", err)
    60  	}
    61  
    62  	// List the existing user keychains. There likely won't be any, but for
    63  	// robustness, we preserve them in the search path.
    64  	stdout.Reset()
    65  	stderr.Reset()
    66  	err = bio.Run(ctx, &biome.Invocation{
    67  		Argv:   []string{"security", "list-keychains", "-d", "user"},
    68  		Stdout: stdout,
    69  		Stderr: stderr,
    70  	})
    71  	if err != nil {
    72  		if stderr.Len() > 0 {
    73  			// stderr will almost certainly end in '\n'.
    74  			return fmt.Errorf("ensure build environment keychain: %s%w", stderr, err)
    75  		}
    76  		return fmt.Errorf("ensure build environment keychain: %w", err)
    77  	}
    78  	keychainList := parseKeychainOutput(stdout.String())
    79  
    80  	// Create a passwordless keychain.
    81  	const keychainName = "login.keychain"
    82  	if err := runCommand(ctx, bio, "security", "create-keychain", "-p", "", keychainName); err != nil {
    83  		return fmt.Errorf("ensure build environment keychain: %w", err)
    84  	}
    85  
    86  	// The keychain must be added to the search path.
    87  	// See https://stackoverflow.com/questions/20391911/os-x-keychain-not-visible-to-keychain-access-app-in-mavericks
    88  	//
    89  	// We prepend it to the search path so that Fastlane picks it up:
    90  	// https://github.com/fastlane/fastlane/blob/832e3e4a19d9cff5d5a14a61e9614b5659327427/fastlane_core/lib/fastlane_core/cert_checker.rb#L133-L134
    91  	searchPathArgs := []string{"security", "list-keychains", "-d", "user", "-s", keychainName}
    92  	for _, k := range keychainList {
    93  		searchPathArgs = append(searchPathArgs, filepath.Base(k))
    94  	}
    95  	if err := runCommand(ctx, bio, searchPathArgs...); err != nil {
    96  		return fmt.Errorf("ensure build environment keychain: %w", err)
    97  	}
    98  
    99  	// Set the new keychain as the default.
   100  	if err := runCommand(ctx, bio, "security", "default-keychain", "-s", keychainName); err != nil {
   101  		return fmt.Errorf("ensure build environment keychain: %w", err)
   102  	}
   103  	return nil
   104  }
   105  
   106  func parseKeychainOutput(out string) []string {
   107  	lines := strings.Split(out, "\n")
   108  	if lines[len(lines)-1] == "" {
   109  		lines = lines[:len(lines)-1]
   110  	}
   111  	paths := make([]string, 0, len(lines))
   112  	for _, line := range lines {
   113  		line = strings.TrimSpace(line)
   114  		if !strings.HasPrefix(line, `"`) || !strings.HasSuffix(line, `"`) {
   115  			continue
   116  		}
   117  		paths = append(paths, line[1:len(line)-1])
   118  	}
   119  	return paths
   120  }