├── .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 |
--------------------------------------------------------------------------------