diff --git a/plugins/veracrypt/plugin.go b/plugins/veracrypt/plugin.go new file mode 100644 index 00000000..1a683071 --- /dev/null +++ b/plugins/veracrypt/plugin.go @@ -0,0 +1,22 @@ +package veracrypt + +import ( + "github.com/1Password/shell-plugins/sdk" + "github.com/1Password/shell-plugins/sdk/schema" +) + +func New() schema.Plugin { + return schema.Plugin{ + Name: "veracrypt", + Platform: schema.PlatformInfo{ + Name: "VeraCrypt", + Homepage: sdk.URL("https://www.veracrypt.fr"), + }, + Credentials: []schema.CredentialType{ + VolumePassword(), + }, + Executables: []schema.Executable{ + VeraCryptCLI(), + }, + } +} \ No newline at end of file diff --git a/plugins/veracrypt/veracrypt.go b/plugins/veracrypt/veracrypt.go new file mode 100644 index 00000000..2f7cf764 --- /dev/null +++ b/plugins/veracrypt/veracrypt.go @@ -0,0 +1,25 @@ +package veracrypt + +import ( + "github.com/1Password/shell-plugins/sdk" + "github.com/1Password/shell-plugins/sdk/needsauth" + "github.com/1Password/shell-plugins/sdk/schema" + +) + +func VeraCryptCLI() schema.Executable { + return schema.Executable{ + Name: "VeraCrypt CLI", + Runs: []string{"veracrypt"}, + DocsURL: sdk.URL("https://www.veracrypt.fr/en/Documentation.html"), + NeedsAuth: needsauth.IfAll( + needsauth.NotForHelpOrVersion(), + needsauth.NotWithoutArgs(), + ), + Uses: []schema.CredentialUsage{ + { + Name: sdk.CredentialName("Volume Password"), + }, + }, + } +} \ No newline at end of file diff --git a/plugins/veracrypt/volume_password.go b/plugins/veracrypt/volume_password.go new file mode 100644 index 00000000..10d6c7b1 --- /dev/null +++ b/plugins/veracrypt/volume_password.go @@ -0,0 +1,86 @@ +package veracrypt + +import ( + "context" + "fmt" + + "github.com/1Password/shell-plugins/sdk" + "github.com/1Password/shell-plugins/sdk/importer" + "github.com/1Password/shell-plugins/sdk/schema" + + "github.com/1Password/shell-plugins/sdk/schema/fieldname" +) + +func VolumePassword() schema.CredentialType { + return schema.CredentialType{ + Name: sdk.CredentialName("Volume Password"), + DocsURL: sdk.URL("https://www.veracrypt.fr/en/Documentation.html"), + ManagementURL: sdk.URL("https://www.veracrypt.fr/en/Main.html"), + Fields: []schema.CredentialField{ + { + Name: fieldname.Password, + MarkdownDescription: "Password used to mount a VeraCrypt volume.", + Secret: true, + }, + { + Name: sdk.FieldName("Volume"), + MarkdownDescription: "Path to the VeraCrypt volume file.", + Secret: false, + Optional: true, + }, + }, + DefaultProvisioner: volumePasswordProvisioner(), + Importer: importer.TryAll( + importer.TryEnvVarPair(defaultEnvVarMapping), + TryVeraCryptConfigFile(), + ), + } +} + +type volumePasswordProv struct{} + +func volumePasswordProvisioner() sdk.Provisioner { + return volumePasswordProv{} +} + +func (p volumePasswordProv) Description() string { + return "Provision password as command-line arguments" +} + +func (p volumePasswordProv) Provision(ctx context.Context, in sdk.ProvisionInput, out *sdk.ProvisionOutput) { + password, ok := in.ItemFields[fieldname.Password] + if !ok || password == "" { + out.AddError(fmt.Errorf("password is required")) + out.CommandLine = []string{} + return + } + args := []string{"-p", password, "--non-interactive"} + if len(out.CommandLine) == 0 { + out.CommandLine = args + return + } + insertAt := len(out.CommandLine) + for i, arg := range out.CommandLine { + if len(arg) > 0 && arg[0] != '-' { + insertAt = i + break + } + } + newCmd := make([]string, 0, len(out.CommandLine)+len(args)) + newCmd = append(newCmd, out.CommandLine[:insertAt]...) + newCmd = append(newCmd, args...) + newCmd = append(newCmd, out.CommandLine[insertAt:]...) + out.CommandLine = newCmd +} + +func (p volumePasswordProv) Deprovision(ctx context.Context, in sdk.DeprovisionInput, out *sdk.DeprovisionOutput) { +} + +var defaultEnvVarMapping = map[string]sdk.FieldName{ + "VERACRYPT_PASSWORD": fieldname.Password, +} + +func TryVeraCryptConfigFile() sdk.Importer { + return importer.TryFile("~/.VeraCrypt/Config", func(ctx context.Context, contents importer.FileContents, in sdk.ImportInput, out *sdk.ImportAttempt) { + }) +} \ No newline at end of file diff --git a/plugins/veracrypt/volume_password_test.go b/plugins/veracrypt/volume_password_test.go new file mode 100644 index 00000000..b4ada03d --- /dev/null +++ b/plugins/veracrypt/volume_password_test.go @@ -0,0 +1,80 @@ +package veracrypt + +import ( + "testing" + + "github.com/1Password/shell-plugins/sdk" + "github.com/1Password/shell-plugins/sdk/plugintest" + "github.com/1Password/shell-plugins/sdk/schema/fieldname" +) + +func TestVolumePasswordProvisioner(t *testing.T) { + plugintest.TestProvisioner(t, VolumePassword().DefaultProvisioner, map[string]plugintest.ProvisionCase{ + "password flag inserted before positional args": { + ItemFields: map[sdk.FieldName]string{ + fieldname.Password: "TestPassword123!", + }, + CommandLine: []string{"-t", "--mount", "/tmp/vol", "/mnt/point"}, + ExpectedOutput: sdk.ProvisionOutput{ + CommandLine: []string{"-t", "--mount", "-p", "TestPassword123!", "--non-interactive", "/tmp/vol", "/mnt/point"}, + }, + }, + "flags inserted before mount point": { + ItemFields: map[sdk.FieldName]string{ + fieldname.Password: "Secret456!", + }, + CommandLine: []string{"--dismount", "/mnt/point"}, + ExpectedOutput: sdk.ProvisionOutput{ + CommandLine: []string{"--dismount", "-p", "Secret456!", "--non-interactive", "/mnt/point"}, + }, + }, + "empty password returns error": { + ItemFields: map[sdk.FieldName]string{ + fieldname.Password: "", + }, + ExpectedOutput: sdk.ProvisionOutput{ + CommandLine: []string{}, + Diagnostics: sdk.Diagnostics{ + Errors: []sdk.Error{ + {Message: "password is required"}, + }, + }, + }, + }, + "volume field stored but not in command line": { + ItemFields: map[sdk.FieldName]string{ + fieldname.Password: "VolumePass789!", + "Volume": "/path/to/volume.tc", + }, + CommandLine: []string{"-t", "--mount"}, + ExpectedOutput: sdk.ProvisionOutput{ + CommandLine: []string{"-t", "--mount", "-p", "VolumePass789!", "--non-interactive"}, + }, + }, + "no command line uses flags as provided": { + ItemFields: map[sdk.FieldName]string{ + fieldname.Password: "MySecret123!", + }, + ExpectedOutput: sdk.ProvisionOutput{ + CommandLine: []string{"-p", "MySecret123!", "--non-interactive"}, + }, + }, + }) +} + +func TestVolumePasswordImporter(t *testing.T) { + plugintest.TestImporter(t, VolumePassword().Importer, map[string]plugintest.ImportCase{ + "environment": { + Environment: map[string]string{ + "VERACRYPT_PASSWORD": "TestPassword123!", + }, + ExpectedCandidates: []sdk.ImportCandidate{ + { + Fields: map[sdk.FieldName]string{ + fieldname.Password: "TestPassword123!", + }, + }, + }, + }, + }) +}