├── .github └── workflows │ ├── check.yml │ └── test.yml ├── .gitignore ├── LICENSE ├── README.md ├── example_test.go ├── go.mod └── secret.go /.github/workflows/check.yml: -------------------------------------------------------------------------------- 1 | name: check 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | - master 8 | pull_request: 9 | 10 | jobs: 11 | golangci-lint: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/setup-go@v5 15 | - uses: actions/checkout@v4 16 | - uses: golangci/golangci-lint-action@v6 17 | with: 18 | version: v1.58 19 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | - master 8 | pull_request: 9 | 10 | jobs: 11 | go-test: 12 | strategy: 13 | matrix: 14 | go-version: [1.16.x, 1.23.x] 15 | os: [ubuntu-latest] 16 | runs-on: ${{ matrix.os }} 17 | steps: 18 | - name: Install Go 19 | uses: actions/setup-go@v5 20 | with: 21 | go-version: ${{ matrix.go-version }} 22 | - name: Checkout code 23 | uses: actions/checkout@v4 24 | - name: Test 25 | run: go test -v -cover ./... 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Dependency directories (remove the comment below to include it) 15 | # vendor/ 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Ravi Shekhar Jethani 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![GoDev](https://img.shields.io/static/v1?label=godev&message=reference&color=00add8)](https://pkg.go.dev/github.com/rsjethani/secret/v3) 2 | [![Build Status](https://github.com/rsjethani/secret/actions/workflows/test.yml/badge.svg?branch=main)](https://github.com/rsjethani/secret/actions) 3 | [![Go Report Card](https://goreportcard.com/badge/github.com/rsjethani/secret)](https://goreportcard.com/report/github.com/rsjethani/secret) 4 | 5 | # Installation 6 | ``` 7 | go get github.com/rsjethani/secret/v3 8 | ``` 9 | 10 | # What secret is? 11 | It provides simple Go types like `Text` to securely store secrets. For example: 12 | ```go 13 | type Login struct { 14 | User string 15 | Password secret.Text 16 | } 17 | ``` 18 | The encapsulated secret remains inaccessible to operations like printing, logging, JSON serialization etc. A (customizable) redact hint like `*****` is returned instead. The only way to access the actual secret value is by asking explicitly via the `.Secret()` method. See package documentation more reference and examples. 19 | 20 | # What secret is not? 21 | - It is not a secret management service or your local password manager. 22 | - It is not a Go client to facilitate communication with secret managers like Hashicorp Vault, AWS secret Manager etc. Checkout [teller](https://github.com/spectralops/teller) if that is what you are looking for. 23 | 24 | # Versions 25 | 26 | ### v3 27 | Current version, same functionality as v2 but much cleaner API. 28 | 29 | ### v2 30 | Perfectly usable but no longer maintained. 31 | 32 | ### v1 33 | Deprecated. 34 | -------------------------------------------------------------------------------- /example_test.go: -------------------------------------------------------------------------------- 1 | package secret_test 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | 7 | "github.com/rsjethani/secret/v3" 8 | ) 9 | 10 | func ExampleNew() { 11 | s := secret.New("$ecre!") 12 | fmt.Println(s, s.Secret()) 13 | 14 | // Output: ***** $ecre! 15 | } 16 | 17 | func ExampleRedactAs() { 18 | s := secret.New("$ecre!", secret.RedactAs(secret.FiveX)) 19 | fmt.Println(s, s.Secret()) 20 | 21 | s = secret.New("$ecre!", secret.RedactAs(secret.Redacted)) 22 | fmt.Println(s, s.Secret()) 23 | 24 | s = secret.New("$ecre!", secret.RedactAs("my redact hint")) 25 | fmt.Println(s, s.Secret()) 26 | 27 | // Output: 28 | // XXXXX $ecre! 29 | // [REDACTED] $ecre! 30 | // my redact hint $ecre! 31 | } 32 | 33 | func ExampleText_MarshalText() { 34 | login := struct { 35 | User string 36 | Password secret.Text 37 | }{ 38 | User: "John", 39 | Password: secret.New("shh!"), 40 | } 41 | 42 | bytes, err := json.Marshal(&login) 43 | if err != nil { 44 | panic(err) 45 | } 46 | 47 | fmt.Println(string(bytes)) 48 | 49 | // Output: {"User":"John","Password":"*****"} 50 | } 51 | 52 | func ExampleText_UnmarshalText() { 53 | login := struct { 54 | User string 55 | Password secret.Text 56 | }{} 57 | 58 | err := json.Unmarshal([]byte(`{"User":"John","Password":"$ecre!"}`), &login) 59 | if err != nil { 60 | panic(err) 61 | } 62 | 63 | fmt.Printf("%+v\n", login) 64 | fmt.Println(login.Password.Secret()) 65 | 66 | // Output: 67 | // {User:John Password:*****} 68 | // $ecre! 69 | } 70 | 71 | func ExampleEqual() { 72 | // Empty Texts are equal. 73 | fmt.Println(secret.Equal(secret.Text{}, secret.Text{})) 74 | 75 | // Initialsed Text is not equal to an empty one. 76 | fmt.Println(secret.Equal(secret.New("hello"), secret.Text{})) 77 | 78 | // Texts with different secret strings are not equal. 79 | fmt.Println(secret.Equal(secret.New("hello"), secret.New("world"))) 80 | 81 | // Texts with different redact strings but same secret string are equal. 82 | fmt.Println(secret.Equal(secret.New("hello"), secret.New("hello", secret.RedactAs(secret.FiveX)))) 83 | 84 | // Output: 85 | // true 86 | // false 87 | // false 88 | // true 89 | } 90 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/rsjethani/secret/v3 2 | 3 | go 1.16 4 | -------------------------------------------------------------------------------- /secret.go: -------------------------------------------------------------------------------- 1 | package secret 2 | 3 | // Text provides a way to safely store your secret string until you actually need it. Operations 4 | // like printing and serializing see a proxy/redact string there by avoiding leaking the secret. 5 | // Once created, the instance is readonly except for the [Text.UnmarshalText] operation, but that 6 | // too only modifies the local copy. Hence the type is concurrent safe. 7 | type Text struct { 8 | secret *string 9 | redact *string 10 | } 11 | 12 | // New returns [Text] for the secret with [FiveStar] as the default redact string. Provide options 13 | // like [RedactAs] to modify default behavior. 14 | func New(secret string, options ...func(*Text)) Text { 15 | tx := Text{ 16 | secret: new(string), 17 | redact: new(string), 18 | } 19 | 20 | *tx.secret = secret 21 | *tx.redact = FiveStar 22 | 23 | for _, o := range options { 24 | o(&tx) 25 | } 26 | 27 | return tx 28 | } 29 | 30 | // RedactAs is a functional option to set r as the redact string for [Text]. You can use one of 31 | // the common redact strings provided with this package like [FiveX] or provide your own. 32 | func RedactAs(r string) func(*Text) { 33 | return func(t *Text) { 34 | *t.redact = r 35 | } 36 | } 37 | 38 | // Some common redact strings. 39 | const ( 40 | FiveX string = "XXXXX" 41 | FiveStar string = "*****" 42 | Redacted string = "[REDACTED]" 43 | ) 44 | 45 | // String implements the [fmt.Stringer] interface and returns only the redact string. This prevents 46 | // the actual secret string from being sent to std*, logs etc. 47 | func (tx Text) String() string { 48 | if tx.redact != nil { 49 | return *tx.redact 50 | } 51 | return FiveStar 52 | } 53 | 54 | // Secret returns the actual secret string stored inside [Text]. 55 | func (tx Text) Secret() string { 56 | if tx.secret != nil { 57 | return *tx.secret 58 | } 59 | return "" 60 | } 61 | 62 | // MarshalText implements [encoding.TextMarshaler]. It marshals redact string into bytes rather than 63 | // the actual secret value. 64 | func (tx Text) MarshalText() ([]byte, error) { 65 | return []byte(tx.String()), nil 66 | } 67 | 68 | // UnmarshalText implements [encoding.TextUnmarshaler]. It unmarshals b into receiver's new secret 69 | // value. If redact string is present then it is reused. 70 | func (tx *Text) UnmarshalText(b []byte) error { 71 | s := string(b) 72 | 73 | // If the original redact is not nil then use it otherwise fallback to default. 74 | if tx.redact != nil { 75 | *tx = New(s, RedactAs(*tx.redact)) 76 | } else { 77 | *tx = New(s) 78 | } 79 | return nil 80 | } 81 | 82 | // Equal returns true if both arguments have the same secret regardless of the redact strings. 83 | func Equal(tx1, tx2 Text) bool { 84 | // If both pointers are equal then it means either both are nil or point to same value. 85 | if tx1.secret == tx2.secret { 86 | return true 87 | } 88 | 89 | // If we are here then it means the two pointers have different values hence return false 90 | // if any one of them is nil. 91 | if tx1.secret == nil || tx2.secret == nil { 92 | return false 93 | } 94 | 95 | // If we are here then it means both pointers are not nil hence compare the values pointed by them. 96 | return *tx1.secret == *tx2.secret 97 | } 98 | --------------------------------------------------------------------------------