diff --git a/.gitignore b/.gitignore index 8e22fa5..18b7e42 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,7 @@ .env /azcrypto /azcrypto.exe +/azk +/azk.exe /infra/out/ +go.work.sum diff --git a/cmd/azk/azk.go b/cmd/azk/azk.go new file mode 100644 index 0000000..724e5cd --- /dev/null +++ b/cmd/azk/azk.go @@ -0,0 +1,231 @@ +package main + +import ( + "context" + "encoding/base64" + "errors" + "flag" + "fmt" + "io" + "os" + + "github.com/Azure/azure-sdk-for-go/sdk/azcore" + "github.com/Azure/azure-sdk-for-go/sdk/azcore/tracing" + "github.com/Azure/azure-sdk-for-go/sdk/azidentity" + "github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys" + "github.com/Azure/azure-sdk-for-go/sdk/tracing/azotel" + "github.com/heaths/azcrypto" + "github.com/mattn/go-isatty" + "go.opentelemetry.io/otel/exporters/stdout/stdouttrace" + "go.opentelemetry.io/otel/sdk/trace" +) + +var ( + // Commands (openssl-like) + encryptFlag = flag.Bool("encrypt", false, "Encrypt with public key") + decryptFlag = flag.Bool("decrypt", false, "Decrypt with private key") + signFlag = flag.Bool("sign", false, "Sign with private key") + verifyFlag = flag.Bool("verify", false, "Verify with public key") + + // Key options + keyIDFlag = flag.String("kid", "", "Key ID (URL)") + pkcsFlag = flag.Bool("pkcs", true, "PKCS#1 v1.5 padding (default)") + oaepFlag = flag.Bool("oaep", false, "PKCS#1 OAEP padding") + + // Input options + inFlag = flag.String("in", "", "Input file `infile`") + + // Output options + outFlag = flag.String("out", "", "Output file `outfile`") + base64Flag = flag.Bool("base64", false, "Base64 encode/decode, depending on encryption flag") + debugFlag = flag.Bool("debug", false, "Verbose output") + + // Errors + errCommand = errors.New("one of encrypt, decrypt, sign, or verify is required") + errKeyID = errors.New("keyID is required") +) + +func main() { + var err error + flag.Parse() + + type command func(client *azcrypto.Client, r io.Reader, w io.Writer) error + var fn command + cmds := map[*bool]command{ + encryptFlag: encrypt, + decryptFlag: decrypt, + signFlag: sign, + verifyFlag: verify, + } + for k, v := range cmds { + if isSet(k) { + if fn != nil { + exit(errCommand) + } + fn = v + } + } + if fn == nil { + exit(errCommand) + } + + if keyIDFlag == nil { + exit(errKeyID) + } + + // Open/create the input and output files, or stdin/stdout. + var r io.Reader + r = os.Stdin + if inFlag != nil && *inFlag != "" { + r, err = os.Open(*inFlag) + if err != nil { + exit(err) + } + } + + var w io.WriteCloser + w = os.Stdout + if outFlag != nil && *outFlag != "" { + w, err = os.Create(*outFlag) + if err != nil { + exit(err) + } + } else { + // Make sure prompt is on new line. + defer w.Write([]byte{'\n'}) + } + if isSet(base64Flag) { + w = base64.NewEncoder(base64.StdEncoding, w) + defer w.Close() + } + + // Create the credentials and client to pass to the command. + credential, err := azidentity.NewDefaultAzureCredential(nil) + if err != nil { + exit(err) + } + + options := &azcrypto.ClientOptions{ + ClientOptions: azkeys.ClientOptions{ + ClientOptions: azcore.ClientOptions{ + TracingProvider: tracingProvider(), + }, + }, + } + client, err := azcrypto.NewClient(*keyIDFlag, credential, options) + if err != nil { + exit(err) + } + + err = fn(client, r, w) + if err != nil { + exit(err) + } +} + +func exit(err error) { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) +} + +func isSet(b *bool) bool { + return b != nil && *b +} + +func tracingProvider() tracing.Provider { + if !isSet(debugFlag) { + return tracing.Provider{} + } + + exp, err := stdouttrace.New(stdouttrace.WithWriter(colorize(os.Stderr))) + if err != nil { + exit(err) + } + + tracer := trace.NewTracerProvider( + // Trace to stderr immediately. + trace.WithSpanProcessor(trace.NewSimpleSpanProcessor(exp)), + ) + + return azotel.NewTracingProvider(tracer, nil) +} + +func encrypt(client *azcrypto.Client, r io.Reader, w io.Writer) error { + plaintext, err := io.ReadAll(r) + if err != nil { + return err + } + + alg, err := encryptionAlgorithm() + if err != nil { + return err + } + + result, err := client.Encrypt(context.Background(), alg, plaintext, nil) + if err != nil { + return err + } + + _, err = w.Write(result.Ciphertext) + return err +} + +func decrypt(client *azcrypto.Client, r io.Reader, w io.Writer) error { + ciphertext, err := io.ReadAll(r) + if err != nil { + return err + } + + alg, err := encryptionAlgorithm() + if err != nil { + return err + } + + result, err := client.Decrypt(context.Background(), alg, ciphertext, nil) + if err != nil { + return err + } + + _, err = w.Write(result.Plaintext) + return err +} + +func sign(client *azcrypto.Client, r io.Reader, w io.Writer) error { + return errors.ErrUnsupported +} + +func verify(client *azcrypto.Client, r io.Reader, w io.Writer) error { + return errors.ErrUnsupported +} + +func encryptionAlgorithm() (azcrypto.EncryptAlgorithm, error) { + switch { + case isSet(oaepFlag): + return azcrypto.EncryptAlgorithmRSAOAEP, nil + case isSet(pkcsFlag): + return azcrypto.EncryptAlgorithmRSA15, nil + } + + return "", errors.New("one of pkcs or oaep required") +} + +func colorize(w io.Writer) io.Writer { + if f, ok := w.(*os.File); ok && isatty.IsTerminal(f.Fd()) { + return colorizer{ + w: w, + } + } + + return w +} + +type colorizer struct { + w io.Writer +} + +func (v colorizer) Write(p []byte) (int, error) { + v.w.Write([]byte("\x1b[2;97m")) + defer v.w.Write([]byte("\x1b[m")) + + return v.w.Write(p) +} diff --git a/cmd/azk/go.mod b/cmd/azk/go.mod new file mode 100644 index 0000000..0bb1b26 --- /dev/null +++ b/cmd/azk/go.mod @@ -0,0 +1,42 @@ +module github.com/heaths/azcrypto/cmd/azk + +go 1.19 + +require ( + github.com/Azure/azure-sdk-for-go/sdk/azcore v1.9.0-beta.1 + github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.3.1 + github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.1.0-beta.1 + github.com/Azure/azure-sdk-for-go/sdk/tracing/azotel v0.2.0 + github.com/heaths/azcrypto v1.0.0 + github.com/mattn/go-isatty v0.0.20 + go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.19.0 + go.opentelemetry.io/otel/sdk v1.19.0 +) + +require ( + github.com/Azure/azure-sdk-for-go/sdk/internal v1.3.0 // indirect + github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.0.0 // indirect + github.com/AzureAD/microsoft-authentication-library-for-go v1.1.1 // indirect + github.com/azure/azure-dev v0.0.0-20230718204335-175a4da25e48 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/go-logr/logr v1.2.4 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/golang-jwt/jwt/v5 v5.0.0 // indirect + github.com/google/uuid v1.3.0 // indirect + github.com/joho/godotenv v1.5.1 // indirect + github.com/kylelemons/godebug v1.1.0 // indirect + github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/sethvargo/go-retry v0.2.4 // indirect + github.com/stretchr/testify v1.8.4 // indirect + go.opentelemetry.io/otel v1.19.0 // indirect + go.opentelemetry.io/otel/metric v1.19.0 // indirect + go.opentelemetry.io/otel/trace v1.19.0 // indirect + golang.org/x/crypto v0.13.0 // indirect + golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1 // indirect + golang.org/x/net v0.15.0 // indirect + golang.org/x/sys v0.12.0 // indirect + golang.org/x/text v0.13.0 // indirect + gopkg.in/dnaeon/go-vcr.v3 v3.1.2 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/cmd/azk/go.sum b/cmd/azk/go.sum new file mode 100644 index 0000000..c5b0c03 --- /dev/null +++ b/cmd/azk/go.sum @@ -0,0 +1,78 @@ +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.9.0-beta.1 h1:ODs3brnqQM99Tq1PffODpAViYv3Bf8zOg464MU7p5ew= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.9.0-beta.1/go.mod h1:3Ug6Qzto9anB6mGlEdgYMDF5zHQ+wwhEaYR4s17PHMw= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.3.1 h1:LNHhpdK7hzUcx/k1LIcuh5k7k1LGIWLQfCjaneSj7Fc= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.3.1/go.mod h1:uE9zaUfEQT/nbQjVi2IblCG9iaLtZsuYZ8ne+PuQ02M= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.3.0 h1:sXr+ck84g/ZlZUOZiNELInmMgOsuGwdjjVkEIde0OtY= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.3.0/go.mod h1:okt5dMMTOFjX/aovMlrjvvXoPMBVSPzk9185BT0+eZM= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.1.1 h1:7CBQ+Ei8SP2c6ydQTGCCrS35bDxgTMfoP2miAwK++OU= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.1.0-beta.1 h1:eprnLetnMQkjvGpO8OtCERU1TuNAxEeuPNP03WGRalI= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.1.0-beta.1/go.mod h1:f35qkpdLIv6wbJIfw5PlbS5mWaz1q82oVpKYzLUdo+s= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.0.0 h1:D3occbWoio4EBLkbkevetNMAVX197GkzbUMtqjGWn80= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.0.0/go.mod h1:bTSOgj05NGRuHHhQwAdPnYr9TOdNmKlZTgGLL6nyAdI= +github.com/Azure/azure-sdk-for-go/sdk/tracing/azotel v0.2.0 h1:YKBjwPHQqOOIZ2TijXDnGylbf70M4moGJsdU40MKZ58= +github.com/Azure/azure-sdk-for-go/sdk/tracing/azotel v0.2.0/go.mod h1:apWvi7CuVOuSuGn4h8cSNV5gHs+vhLTicndWY7p2Ggk= +github.com/AzureAD/microsoft-authentication-library-for-go v1.1.1 h1:WpB/QDNLpMw72xHJc34BNNykqSOeEJDAWkhf0u12/Jk= +github.com/AzureAD/microsoft-authentication-library-for-go v1.1.1/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= +github.com/azure/azure-dev v0.0.0-20230718204335-175a4da25e48 h1:IvPqn1m6Z4nSgjstXlg7F8l7WTwCxB8PELDejuZ+juA= +github.com/azure/azure-dev v0.0.0-20230718204335-175a4da25e48/go.mod h1:vc9yod/BaAY5/zo8+OhPAaY/+eaDN/pVuZkY+QQXXLo= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dnaeon/go-vcr v1.2.0 h1:zHCHvJYTMh1N7xnV7zf1m1GPBF9Ad0Jk/whtQ1663qI= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ= +github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/golang-jwt/jwt/v5 v5.0.0 h1:1n1XNM9hk7O9mnQoNBGolZvzebBQ7p93ULHRc28XJUE= +github.com/golang-jwt/jwt/v5 v5.0.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/heaths/azcrypto v1.0.0 h1:Jf1vgR8KL6TWAbzL2BWOoXjQ43tuSmnhOlZ9aSlaYt4= +github.com/heaths/azcrypto v1.0.0/go.mod h1:SkGzm1I4OT998nD2M+9NA6GQ7MrOJWoB7USQs+7Zwv4= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= +github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 h1:KoWmjvw+nsYOo29YJK9vDA65RGE3NrOnUtO7a+RF9HU= +github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/sethvargo/go-retry v0.2.4 h1:T+jHEQy/zKJf5s95UkguisicE0zuF9y7+/vgz08Ocec= +github.com/sethvargo/go-retry v0.2.4/go.mod h1:1afjQuvh7s4gflMObvjLPaWgluLLyhA1wmVZ6KLpICw= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +go.opentelemetry.io/otel v1.19.0 h1:MuS/TNf4/j4IXsZuJegVzI1cwut7Qc00344rgH7p8bs= +go.opentelemetry.io/otel v1.19.0/go.mod h1:i0QyjOq3UPoTzff0PJB2N66fb4S0+rSbSB15/oyH9fY= +go.opentelemetry.io/otel/exporters/jaeger v1.16.0 h1:YhxxmXZ011C0aDZKoNw+juVWAmEfv/0W2XBOv9aHTaA= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.19.0 h1:Nw7Dv4lwvGrI68+wULbcq7su9K2cebeCUrDjVrUJHxM= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.19.0/go.mod h1:1MsF6Y7gTqosgoZvHlzcaaM8DIMNZgJh87ykokoNH7Y= +go.opentelemetry.io/otel/metric v1.19.0 h1:aTzpGtV0ar9wlV4Sna9sdJyII5jTVJEvKETPiOKwvpE= +go.opentelemetry.io/otel/metric v1.19.0/go.mod h1:L5rUsV9kM1IxCj1MmSdS+JQAcVm319EUrDVLrt7jqt8= +go.opentelemetry.io/otel/sdk v1.19.0 h1:6USY6zH+L8uMH8L3t1enZPR3WFEmSTADlqldyHtJi3o= +go.opentelemetry.io/otel/sdk v1.19.0/go.mod h1:NedEbbS4w3C6zElbLdPJKOpJQOrGUJ+GfzpjUvI0v1A= +go.opentelemetry.io/otel/trace v1.19.0 h1:DFVQmlVbfVeOuBRrwdtaehRrWiL1JoVs9CPIQ1Dzxpg= +go.opentelemetry.io/otel/trace v1.19.0/go.mod h1:mfaSyvGyEJEI0nyV2I4qhNQnbBOUUmYZpYojqMnX2vo= +golang.org/x/crypto v0.13.0 h1:mvySKfSWJ+UKUii46M40LOvyWfN0s2U+46/jDd0e6Ck= +golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= +golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1 h1:MGwJjxBy0HJshjDNfLsYO8xppfqWlA5ZT9OhtUUhTNw= +golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc= +golang.org/x/net v0.15.0 h1:ugBLEUaxABaB5AJqW9enI0ACdci2RUd4eP51NTBvuJ8= +golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= +golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b h1:QRR6H1YWRnHb4Y/HeNFCTJLFVxaq6wH4YuVdsUOr75U= +gopkg.in/dnaeon/go-vcr.v3 v3.1.2 h1:F1smfXBqQqwpVifDfUBQG6zzaGjzT+EnVZakrOdr5wA= +gopkg.in/dnaeon/go-vcr.v3 v3.1.2/go.mod h1:2IMOnnlx9I6u9x+YBsM3tAMx6AlOxnJ0pWxQAzZ79Ag= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/go.work b/go.work new file mode 100644 index 0000000..f481ae1 --- /dev/null +++ b/go.work @@ -0,0 +1,6 @@ +go 1.19 + +use ( + . + ./cmd/azk +)