├── .github └── workflows │ ├── ci.yml │ └── release.yml ├── .gitignore ├── LICENSE ├── README.md ├── andotp ├── andotp.go └── andotp_test.go ├── build.sh ├── go.mod ├── go.sum └── main.go /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: "CI" 2 | 3 | on: 4 | push: 5 | pull_request: 6 | workflow_dispatch: 7 | 8 | jobs: 9 | test: 10 | name: CI/CD Test 11 | # https://github.com/actions/virtual-environments/ 12 | runs-on: ubuntu-latest 13 | steps: 14 | 15 | - name: 🛎️ Checkout 16 | uses: actions/checkout@v3 17 | 18 | - name: 🔧 Setup go 19 | uses: actions/setup-go@v4 20 | with: 21 | go-version-file: 'go.mod' 22 | 23 | # Test 24 | - name: 🌡️ Test 25 | run: go run main.go -h -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v*.*.*" 7 | 8 | jobs: 9 | build: 10 | name: Build 11 | # https://github.com/actions/virtual-environments/ 12 | runs-on: ubuntu-latest 13 | steps: 14 | 15 | - name: 🛎️ Checkout 16 | uses: actions/checkout@v3 17 | 18 | # https://github.com/marketplace/actions/setup-go-environment 19 | - name: 🔧 Setup go 20 | uses: actions/setup-go@v4 21 | with: 22 | go-version-file: 'go.mod' 23 | 24 | - name: 🍳 Build 25 | run: bash build.sh 26 | 27 | # Test binary 28 | - name: 🌡️ Test 29 | run: chmod +x go-andotp-linux-x86_64 && ./go-andotp-linux-x86_64 -h 30 | 31 | # Upload binaries 32 | # https://github.com/marketplace/actions/upload-a-build-artifact 33 | - name: 📤 Upload 34 | uses: actions/upload-artifact@v3 35 | with: 36 | name: go-andotp 37 | path: go-andotp* 38 | retention-days: 1 39 | 40 | test-linux: 41 | name: Test Linux 42 | needs: build 43 | runs-on: ubuntu-latest 44 | steps: 45 | - name: 🛎️ Checkout 46 | uses: actions/checkout@v3 47 | # Download binaries 48 | # https://github.com/marketplace/actions/download-a-build-artifact 49 | - name: 📥 Download 50 | uses: actions/download-artifact@v3 51 | with: 52 | name: go-andotp 53 | # Test binary 54 | - name: 🌡️ Test 55 | run: chmod +x go-andotp-linux-x86_64 && ./go-andotp-linux-x86_64 -h 56 | 57 | test-macos: 58 | name: Test macOS 59 | needs: build 60 | runs-on: macos-latest 61 | steps: 62 | - name: 🛎️ Checkout 63 | uses: actions/checkout@v3 64 | - name: 📥 Download 65 | uses: actions/download-artifact@v3 66 | with: 67 | name: go-andotp 68 | # Test binary 69 | - name: 🌡️ Test 70 | run: chmod +x go-andotp-macos-x86_64 && ./go-andotp-macos-x86_64 -h 71 | 72 | test-windows: 73 | name: Test Windows 74 | needs: build 75 | runs-on: windows-latest 76 | steps: 77 | - name: 🛎️ Checkout 78 | uses: actions/checkout@v3 79 | - name: 📥 Download 80 | uses: actions/download-artifact@v3 81 | with: 82 | name: go-andotp 83 | # Test binary 84 | - name: 🌡️ Test 85 | run: .\go-andotp-windows-x86_64.exe -h 86 | 87 | release: 88 | name: Release 89 | needs: [test-linux, test-macos, test-windows] 90 | runs-on: ubuntu-latest 91 | steps: 92 | - name: 🛎️ Checkout 93 | uses: actions/checkout@v3 94 | - name: 📥 Download 95 | uses: actions/download-artifact@v3 96 | with: 97 | name: go-andotp 98 | # Release, upload files 99 | # https://github.com/marketplace/actions/gh-release 100 | - name: ✨ Release 101 | uses: softprops/action-gh-release@v1 102 | with: 103 | files: | 104 | go-andotp-linux-x86_64 105 | go-andotp-linux-arm64 106 | go-andotp-macos-x86_64 107 | go-andotp-macos-arm64 108 | go-andotp-windows-x86_64.exe 109 | go-andotp-windows-arm64.exe 110 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | go-andotp* 9 | 10 | # Test binary, built with `go test -c` 11 | *.test 12 | 13 | # Output of the go coverage tool, specifically when used with LiteIDE 14 | *.out 15 | 16 | # Dependency directories (remove the comment below to include it) 17 | # vendor/ 18 | 19 | # Go workspace file 20 | go.work -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Rijul Gulati 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 | # go-andotp 2 | CLI program to encrypt/decrypt [andOTP](https://github.com/andOTP/andOTP) files. 3 | 4 | ## Installation 5 | 6 |
7 | Linux 8 | 9 | Download: 10 | * [x86_64](https://github.com/RijulGulati/go-andotp/releases/latest/download/go-andotp-linux-x86_64) Intel or AMD 64-Bit CPU 11 | ```shell 12 | curl -L "https://github.com/RijulGulati/go-andotp/releases/latest/download/go-andotp-linux-x86_64" \ 13 | -o "go-andotp" && \ 14 | chmod +x "go-andotp" 15 | ``` 16 | * [arm64](https://github.com/RijulGulati/go-andotp/releases/latest/download/go-andotp-linux-arm64) Arm-based 64-Bit CPU (i.e. in Raspberry Pi) 17 | ```shell 18 | curl -L "https://github.com/RijulGulati/go-andotp/releases/latest/download/go-andotp-linux-arm64" \ 19 | -o "go-andotp" && \ 20 | chmod +x "go-andotp" 21 | ``` 22 | 23 | To determine your OS version, run `getconf LONG_BIT` or `uname -m` at the command line. 24 |
25 | 26 |
27 | macOS 28 | 29 | Download: 30 | * [x86_64](https://github.com/RijulGulati/go-andotp/releases/latest/download/go-andotp-macos-x86_64) Intel 64-bit 31 | ```shell 32 | curl -L "https://github.com/RijulGulati/go-andotp/releases/latest/download/go-andotp-macos-x86_64" \ 33 | -o "go-andotp" && \ 34 | chmod +x "go-andotp" 35 | ``` 36 | * [arm64](https://github.com/RijulGulati/go-andotp/releases/latest/download/go-andotp-macos-arm64) Apple silicon 64-bit 37 | ```shell 38 | curl -L "https://github.com/RijulGulati/go-andotp/releases/latest/download/go-andotp-macos-arm64" \ 39 | -o "go-andotp" && \ 40 | chmod +x "go-andotp" 41 | ``` 42 | 43 | To determine your OS version, run `uname -m` at the command line. 44 |
45 | 46 |
47 | Windows 48 | 49 | Download: 50 | * [x86_64](https://github.com/RijulGulati/go-andotp/releases/latest/download/go-andotp-windows-x86_64.exe) Intel or AMD 64-Bit CPU 51 | ```powershell 52 | Invoke-WebRequest -Uri "https://github.com/RijulGulati/go-andotp/releases/latest/download/go-andotp-windows-x86_64.exe" -OutFile "go-andotp.exe" 53 | ``` 54 | * [arm64](https://github.com/RijulGulati/go-andotp/releases/latest/download/go-andotp-windows-arm64.exe) Arm-based 64-Bit CPU 55 | ```powershell 56 | Invoke-WebRequest -Uri "https://github.com/RijulGulati/go-andotp/releases/latest/download/go-andotp-windows-arm64.exe" -OutFile "go-andotp.exe" 57 | ``` 58 | To determine your OS version, run `echo %PROCESSOR_ARCHITECTURE%` at the command line. 59 |
60 | 61 |
62 | Go 63 | 64 | ```shell 65 | go install github.com/grijul/go-andotp 66 | ``` 67 |
68 | 69 | ## Usage 70 | ```text 71 | Usage: go-andotp -i {-e|-d} [-o ] [-p PASSWORD] 72 | 73 | -d Decrypt file 74 | -e Encrypt file. 75 | -i string 76 | Input File 77 | -o string 78 | Output File. If no file is provided, output is printed to STDOUT 79 | -p string 80 | Encryption Password. This option can be skipped to get password prompt. 81 | ``` 82 | 83 | ## Examples 84 | - Encrypt JSON file (Password is asked after hitting ```Enter```. Password is not echoed) 85 | ```shell 86 | go-andotp -e -i file.json -o file.json.aes 87 | ``` 88 | - Encrypt JSON file (Password is entered through CLI) 89 | ```shell 90 | go-andotp -e -i file.json -o file.json.aes -p testpass 91 | ``` 92 | - Decrypt JSON file 93 | ```shell 94 | go-andotp -d -i file.aes.json -o file.json 95 | ``` 96 | - Decrypt JSON file and print json to console 97 | ```shell 98 | go-andotp -d -i file.aes.json 99 | ``` 100 | 101 | ## Using go-andotp as library 102 | go-andotp can be used as library as well. It implements ```Encrypt()``` and ```Decrypt()``` functions to encrypt/decrypt text (respectively). 103 | It's documentation is available at: https://pkg.go.dev/github.com/grijul/go-andotp/andotp 104 | 105 | Example usage: 106 | ```go 107 | import "github.com/grijul/go-andotp/andotp" 108 | 109 | func main() { 110 | andotp.Encrypt(...) 111 | andotp.Decrypt(...) 112 | } 113 | ``` 114 | 115 | ## Build 116 | Compile `go-andotp` on your computer: 117 | 118 | ```shell 119 | go build -o go-andotp main.go 120 | ``` 121 | 122 | To compile `go-andotp` for another platform please set the `GOARCH` and `GOOS` environmental variables. 123 | Example: 124 | ```shell 125 | GOOS=windows GOARCH=amd64 go build -o go-andotp.exe main.go 126 | ``` 127 | 128 | To compile `go-andotp` for Windows, macOS and Linux you can use the script `build.sh`: 129 | ```shell 130 | bash build.sh 131 | ``` 132 | 133 | More help: 134 | 135 | # License 136 | [MIT](https://github.com/grijul/go-andotp/blob/main/LICENSE) 137 | -------------------------------------------------------------------------------- /andotp/andotp.go: -------------------------------------------------------------------------------- 1 | // Package andotp implements functions to encrypt/decrypt andOTP files. 2 | package andotp 3 | 4 | import ( 5 | "crypto/aes" 6 | "crypto/cipher" 7 | "crypto/sha1" 8 | "encoding/binary" 9 | "fmt" 10 | "math/rand" 11 | 12 | "golang.org/x/crypto/pbkdf2" 13 | ) 14 | 15 | const ( 16 | ivLen int = 12 17 | keyLen int = 32 18 | iterationLen int = 4 19 | saltLen int = 12 20 | maxIterations int = 160000 21 | minIterations int = 140000 22 | ) 23 | 24 | // Encrypt encrypts plaintext with password according to andotp encryption standard. 25 | // It returns encrypted byte array and any error encountered. 26 | func Encrypt(plaintext []byte, password string) ([]byte, error) { 27 | 28 | var finalCipher []byte 29 | iter := make([]byte, iterationLen) 30 | iv := make([]byte, ivLen) 31 | salt := make([]byte, saltLen) 32 | 33 | iterations := rand.Intn(maxIterations-minIterations) + minIterations 34 | binary.BigEndian.PutUint32(iter, uint32(iterations)) 35 | 36 | _, err := rand.Read(iv) 37 | if err != nil { 38 | return nil, formatError(err.Error()) 39 | } 40 | 41 | _, err = rand.Read(salt) 42 | if err != nil { 43 | return nil, formatError(err.Error()) 44 | } 45 | 46 | secretKey := pbkdf2.Key([]byte(password), salt, iterations, keyLen, sha1.New) 47 | 48 | block, err := aes.NewCipher(secretKey) 49 | if err != nil { 50 | return nil, formatError(err.Error()) 51 | } 52 | 53 | aesgcm, err := cipher.NewGCM(block) 54 | if err != nil { 55 | return nil, formatError(err.Error()) 56 | } 57 | 58 | cipherText := aesgcm.Seal(nil, iv, plaintext, nil) 59 | 60 | finalCipher = append(finalCipher, iter...) 61 | finalCipher = append(finalCipher, salt...) 62 | finalCipher = append(finalCipher, iv...) 63 | finalCipher = append(finalCipher, cipherText...) 64 | 65 | return finalCipher, nil 66 | 67 | } 68 | 69 | // Decrypt decrypts encryptedtext using password. 70 | // It returns decrypted byte array and any error encountered. 71 | func Decrypt(encryptedtext []byte, password string) ([]byte, error) { 72 | 73 | iterations := encryptedtext[:iterationLen] 74 | salt := encryptedtext[iterationLen : iterationLen+saltLen] 75 | iv := encryptedtext[iterationLen+saltLen : iterationLen+saltLen+ivLen] 76 | cipherText := encryptedtext[iterationLen+saltLen+ivLen:] 77 | iter := int(binary.BigEndian.Uint32(iterations)) 78 | secretKey := pbkdf2.Key([]byte(password), salt, iter, keyLen, sha1.New) 79 | 80 | block, err := aes.NewCipher(secretKey) 81 | if err != nil { 82 | return nil, formatError(err.Error()) 83 | } 84 | 85 | aesgcm, err := cipher.NewGCM(block) 86 | if err != nil { 87 | return nil, formatError(err.Error()) 88 | } 89 | 90 | plaintextbytes, err := aesgcm.Open(nil, iv, cipherText, nil) 91 | if err != nil { 92 | return nil, formatError(err.Error()) 93 | } 94 | 95 | return plaintextbytes, nil 96 | } 97 | 98 | func formatError(e string) error { 99 | return fmt.Errorf("error: %s", e) 100 | } 101 | -------------------------------------------------------------------------------- /andotp/andotp_test.go: -------------------------------------------------------------------------------- 1 | package andotp_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/grijul/go-andotp/andotp" 7 | ) 8 | 9 | const ( 10 | jsonstr string = "[{\"secret\":\"SOMEAWESOMESECRET\",\"issuer\":\"SOMEAWESOMEISSUER\",\"label\":\"TESTLABEL\",\"digits\":6,\"type\":\"TOTP\",\"algorithm\":\"SHA1\",\"thumbnail\":\"Default\",\"last_used\":1000000000000,\"used_frequency\":0,\"period\":30,\"tags\":[]}]" 11 | password string = "testpass" 12 | ) 13 | 14 | func TestEncryptDecrypt(t *testing.T) { 15 | 16 | encryptedtext, err := andotp.Encrypt([]byte(jsonstr), password) 17 | if err != nil { 18 | t.Error(err) 19 | } 20 | 21 | decryptedtext, err := andotp.Decrypt(encryptedtext, password) 22 | if err != nil { 23 | t.Error(err) 24 | } 25 | 26 | if string(decryptedtext) != jsonstr { 27 | t.Error("Encryption/Decryption failed. Text mismatch") 28 | } 29 | 30 | // With wrong password 31 | 32 | encryptedtext, err = andotp.Encrypt([]byte(jsonstr), password) 33 | if err != nil { 34 | t.Error(err) 35 | } 36 | 37 | _, err = andotp.Decrypt(encryptedtext, "someotherpass") 38 | if err != nil { 39 | if err.Error() != "error: cipher: message authentication failed" { 40 | t.Error(err) 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | go version || exit 9 4 | 5 | # Linux 6 | echo "Linux" && \ 7 | GOOS=linux GOARCH=amd64 go build -o go-andotp-linux-x86_64 main.go && \ 8 | GOOS=linux GOARCH=arm64 go build -o go-andotp-linux-arm64 main.go && \ 9 | 10 | # macOS 11 | echo "macOS" && \ 12 | GOOS=darwin GOARCH=amd64 go build -o go-andotp-macos-x86_64 main.go && \ 13 | GOOS=darwin GOARCH=arm64 go build -o go-andotp-macos-arm64 main.go && \ 14 | 15 | # Windows 16 | echo "Windows" && \ 17 | GOOS=windows GOARCH=amd64 go build -o go-andotp-windows-x86_64.exe main.go && \ 18 | GOOS=windows GOARCH=arm64 go build -o go-andotp-windows-arm64.exe main.go && \ 19 | 20 | echo "✅ DONE" 21 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/grijul/go-andotp 2 | 3 | go 1.20 4 | 5 | require ( 6 | golang.org/x/crypto v0.0.0-20210506145944-38f3c27a63bf 7 | golang.org/x/sys v0.0.0-20210601080250-7ecdf8ef093b // indirect 8 | golang.org/x/term v0.0.0-20210503060354-a79de5458b56 9 | ) 10 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | golang.org/x/crypto v0.0.0-20210506145944-38f3c27a63bf h1:B2n+Zi5QeYRDAEodEu72OS36gmTWjgpXr2+cWcBW90o= 2 | golang.org/x/crypto v0.0.0-20210506145944-38f3c27a63bf/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= 3 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 4 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 5 | golang.org/x/sys v0.0.0-20210601080250-7ecdf8ef093b h1:qh4f65QIVFjq9eBURLEYWqaEXmOyqdUyiBSgaXWccWk= 6 | golang.org/x/sys v0.0.0-20210601080250-7ecdf8ef093b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 7 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 8 | golang.org/x/term v0.0.0-20210503060354-a79de5458b56 h1:b8jxX3zqjpqb2LklXPzKSGJhzyxCOZSz8ncv8Nv+y7w= 9 | golang.org/x/term v0.0.0-20210503060354-a79de5458b56/go.mod h1:tfny5GFUkzUvx4ps4ajbZsCe5lw1metzhBm9T3x7oIY= 10 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 11 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 12 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "io/ioutil" 7 | "os" 8 | "syscall" 9 | 10 | "github.com/grijul/go-andotp/andotp" 11 | "golang.org/x/term" 12 | ) 13 | 14 | func main() { 15 | 16 | flag.Usage = func() { 17 | fmt.Printf("Usage: %s -i {-e|-d} [-o ] [-p PASSWORD]\n\n", os.Args[0]) 18 | flag.PrintDefaults() 19 | } 20 | 21 | encryptPtr := flag.Bool("e", false, "Encrypt file.") 22 | decryptPtr := flag.Bool("d", false, "Decrypt file") 23 | inputFilePtr := flag.String("i", "", "Input File") 24 | passwordPtr := flag.String("p", "", "Encryption Password. This option can be skipped to get password prompt.") 25 | outputFilePtr := flag.String("o", "", "Output File. If no file is provided, output is printed to STDOUT") 26 | 27 | flag.Parse() 28 | 29 | if *inputFilePtr == "" { 30 | fmt.Println(formatError("No input file provided\nSee -h for available options.")) 31 | os.Exit(0) 32 | } 33 | 34 | if *encryptPtr && *decryptPtr { 35 | fmt.Println(formatError("Please provide any one of encrypt (-e) or decrypt (-d) flag")) 36 | os.Exit(0) 37 | } 38 | 39 | if *passwordPtr == "" { 40 | *passwordPtr = getPassword() 41 | if *passwordPtr == "" { 42 | fmt.Println(formatError("No password provided.")) 43 | os.Exit(0) 44 | } 45 | } 46 | 47 | if *encryptPtr { 48 | plaintext := readFile(*inputFilePtr) 49 | filecontent, err := andotp.Encrypt(plaintext, *passwordPtr) 50 | 51 | if err != nil { 52 | fmt.Print(err.Error()) 53 | os.Exit(0) 54 | } 55 | 56 | processfile(filecontent, *outputFilePtr) 57 | 58 | } else if *decryptPtr { 59 | encryptedtext := readFile(*inputFilePtr) 60 | filecontent, err := andotp.Decrypt(encryptedtext, *passwordPtr) 61 | if err != nil { 62 | fmt.Print(err.Error()) 63 | os.Exit(0) 64 | } 65 | processfile(filecontent, *outputFilePtr) 66 | 67 | } else { 68 | fmt.Println(formatError("Please provide encrypt (-e) or decrypt (-d) flag")) 69 | os.Exit(0) 70 | } 71 | } 72 | 73 | func processfile(filecontent []byte, outputfile string) { 74 | if outputfile == "" { 75 | fmt.Printf("%s", filecontent) 76 | } else { 77 | ioutil.WriteFile(outputfile, filecontent, 0644) 78 | } 79 | } 80 | 81 | func getPassword() string { 82 | fmt.Print("Password: ") 83 | pass, err := term.ReadPassword(int(syscall.Stdin)) 84 | if err != nil { 85 | fmt.Print(formatError(err.Error())) 86 | } 87 | fmt.Print("\n") 88 | return string(pass) 89 | } 90 | 91 | func readFile(file string) []byte { 92 | f, err := os.ReadFile(file) 93 | if err != nil { 94 | fmt.Println(err.Error()) 95 | os.Exit(1) 96 | } 97 | return f 98 | } 99 | 100 | func formatError(e string) error { 101 | return fmt.Errorf("error: %s", e) 102 | } 103 | --------------------------------------------------------------------------------