diff --git a/plugins/age/age.go b/plugins/age/age.go new file mode 100644 index 00000000..8d184e8d --- /dev/null +++ b/plugins/age/age.go @@ -0,0 +1,26 @@ +package age + +import ( + "github.com/1Password/shell-plugins/sdk" + "github.com/1Password/shell-plugins/sdk/needsauth" + "github.com/1Password/shell-plugins/sdk/schema" + "github.com/1Password/shell-plugins/sdk/schema/credname" +) + +func AgeCLI() schema.Executable { + return schema.Executable{ + Name: "Age CLI", + Runs: []string{"age"}, + DocsURL: sdk.URL("https://age-encryption.org"), + NeedsAuth: needsauth.IfAll( + needsauth.NotForHelpOrVersion(), + needsauth.NotWithoutArgs(), + needsauth.NotForExactArgs("encrypt", "recipient", "armor"), + ), + Uses: []schema.CredentialUsage{ + { + Name: credname.SecretKey, + }, + }, + } +} diff --git a/plugins/age/plugin.go b/plugins/age/plugin.go new file mode 100644 index 00000000..492a1173 --- /dev/null +++ b/plugins/age/plugin.go @@ -0,0 +1,22 @@ +package age + +import ( + "github.com/1Password/shell-plugins/sdk" + "github.com/1Password/shell-plugins/sdk/schema" +) + +func New() schema.Plugin { + return schema.Plugin{ + Name: "age", + Platform: schema.PlatformInfo{ + Name: "Age", + Homepage: sdk.URL("https://age-encryption.org/"), + }, + Credentials: []schema.CredentialType{ + PrivateKey(), + }, + Executables: []schema.Executable{ + AgeCLI(), + }, + } +} diff --git a/plugins/age/private_key.go b/plugins/age/private_key.go new file mode 100644 index 00000000..06d87a1c --- /dev/null +++ b/plugins/age/private_key.go @@ -0,0 +1,32 @@ +package age + +import ( + "github.com/1Password/shell-plugins/sdk" + "github.com/1Password/shell-plugins/sdk/schema" + "github.com/1Password/shell-plugins/sdk/schema/credname" + "github.com/1Password/shell-plugins/sdk/schema/fieldname" +) + +func PrivateKey() schema.CredentialType { + return schema.CredentialType{ + Name: credname.SecretKey, + DocsURL: sdk.URL("https://age-encryption.org/"), + Fields: []schema.CredentialField{ + { + Name: fieldname.PrivateKey, + MarkdownDescription: "Age Private key", + Secret: true, + Composition: &schema.ValueComposition{ + Length: 80, + Charset: schema.Charset{ + Uppercase: true, + Lowercase: true, + Digits: true, + Symbols: true, + }, + }, + }, + }, + DefaultProvisioner: PrivateKeyProvisioner{}, + } +} diff --git a/plugins/age/private_key_provisioner.go b/plugins/age/private_key_provisioner.go new file mode 100644 index 00000000..f4b19f0a --- /dev/null +++ b/plugins/age/private_key_provisioner.go @@ -0,0 +1,50 @@ +package age + +import ( + "context" + "crypto/rand" + "fmt" + "slices" + + "github.com/1Password/shell-plugins/sdk" + "github.com/1Password/shell-plugins/sdk/schema/fieldname" +) + +type PrivateKeyProvisioner struct { +} + +func (p PrivateKeyProvisioner) Provision(_ context.Context, in sdk.ProvisionInput, out *sdk.ProvisionOutput) { + privateKey := in.ItemFields[fieldname.PrivateKey] + + fileName, err := randomFilename() + if err != nil { + out.AddError(fmt.Errorf("generating random file name: %s", err)) + return + } + + secretFilePath := in.FromTempDir(fileName) + out.AddSecretFile(secretFilePath, []byte(privateKey)) + + editedCommandLine := slices.Insert(out.CommandLine, 1, "--identity", secretFilePath) + + if editedCommandLine != nil { + out.CommandLine = editedCommandLine + } +} + +func (p PrivateKeyProvisioner) Deprovision(_ context.Context, _ sdk.DeprovisionInput, _ *sdk.DeprovisionOutput) { + // Nothing to do here: environment variables get wiped automatically when the process exits. +} + +func (p PrivateKeyProvisioner) Description() string { + return "Provision age secret key" +} + +func randomFilename() (string, error) { + b := make([]byte, 16) + _, err := rand.Read(b) + if err != nil { + return "", err + } + return fmt.Sprintf("%x", b), nil +}