├── .codecov.yml ├── .github └── workflows │ ├── release.yml │ ├── test-coverage.yml │ └── test.yml ├── .gitignore ├── LICENSE ├── README.md ├── cmd └── wifiqr │ ├── .goreleaser.yml │ ├── main.go │ └── main_test.go ├── config.go ├── config_test.go ├── docs └── images │ └── qr.png ├── go.mod ├── go.sum ├── wifiqr.go └── wifiqr_test.go /.codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | status: 3 | project: 4 | default: 5 | target: 80% 6 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | 8 | permissions: 9 | contents: write 10 | 11 | jobs: 12 | goreleaser: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@v4 17 | with: 18 | fetch-depth: 0 19 | 20 | - name: Set up Go 21 | uses: actions/setup-go@v5 22 | with: 23 | go-version: 1.22.x 24 | 25 | - name: Run GoReleaser 26 | uses: goreleaser/goreleaser-action@v5 27 | with: 28 | version: latest 29 | workdir: ./cmd/wifiqr 30 | args: release --clean 31 | env: 32 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} -------------------------------------------------------------------------------- /.github/workflows/test-coverage.yml: -------------------------------------------------------------------------------- 1 | name: Test Coverage 2 | 3 | on: 4 | push: 5 | branches: 6 | - '**' 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | test: 13 | runs-on: ubuntu-latest 14 | strategy: 15 | matrix: 16 | go-version: [1.22] 17 | steps: 18 | - name: Setup Go 19 | uses: actions/setup-go@v5 20 | with: 21 | go-version: ${{ matrix.go-version }} 22 | 23 | - name: Checkout code 24 | uses: actions/checkout@v4 25 | 26 | - name: Run coverage 27 | run: go test . -coverprofile=coverage.out -covermode=atomic 28 | 29 | - name: Upload coverage to Codecov 30 | run: bash <(curl -s https://codecov.io/bash) -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | push: 5 | branches: 6 | - '**' 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | test: 13 | runs-on: ubuntu-latest 14 | strategy: 15 | matrix: 16 | go-version: [1.18, 1.22] 17 | steps: 18 | - name: Setup Go 19 | uses: actions/setup-go@v5 20 | with: 21 | go-version: ${{ matrix.go-version }} 22 | 23 | - name: Checkout code 24 | uses: actions/checkout@v4 25 | 26 | - name: Test 27 | run: go test ./... -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode/ 2 | .idea/ 3 | /wifiqr 4 | dist/ 5 | coverage.out -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2020 reugn 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Wi-Fi QR Code Generator 2 | 3 | 4 | 5 | [![Test Status](https://github.com/reugn/wifiqr/workflows/Test/badge.svg)](https://github.com/reugn/wifiqr/actions?query=workflow%3ATest) 6 | [![PkgGoDev](https://pkg.go.dev/badge/github.com/reugn/wifiqr)](https://pkg.go.dev/github.com/reugn/wifiqr) 7 | [![Go Report Card](https://goreportcard.com/badge/github.com/reugn/wifiqr)](https://goreportcard.com/report/github.com/reugn/wifiqr) 8 | [![codecov](https://codecov.io/gh/reugn/wifiqr/branch/main/graph/badge.svg)](https://codecov.io/gh/reugn/wifiqr) 9 | 10 | Create a QR code with your Wi-Fi login details. 11 | 12 | Use Google Lens or other application to scan it and connect automatically. 13 | 14 | ## Installation 15 | 16 | Choose a binary from the [releases](https://github.com/reugn/wifiqr/releases). 17 | 18 | ### Build from Source 19 | 20 | Download and [install Go](https://golang.org/doc/install). 21 | 22 | Install the application: 23 | 24 | ```sh 25 | go install github.com/reugn/wifiqr/cmd/wifiqr@latest 26 | ``` 27 | 28 | See the [go install](https://go.dev/ref/mod#go-install) instructions for more information about the command. 29 | 30 | ## Usage 31 | 32 | ```text 33 | $ wifiqr --help 34 | wifiqr is a WiFi QR code generator 35 | 36 | It is used to create a QR code containing the login details such as 37 | the name, password, and encryption type. This QR code can be scanned 38 | using Google Lens or other QR code reader to connect to the network. 39 | It is Android and iOS compatible. 40 | 41 | If the options necessary for creating the QR code are not given on 42 | the command line, the user will be prompted for the information. 43 | 44 | Usage: 45 | wifiqr [flags] 46 | 47 | Flags: 48 | -h, --help help for wifiqr 49 | --hidden Hidden SSID 50 | -k, --key string Wireless password (pre-shared key / PSK) 51 | -o, --output string PNG file for output (default stdout) 52 | -p, --protocol string Wireless network encryption protocol (WPA2, WPA, WEP, NONE). (default "WPA2") 53 | -s, --size int Image width and height in pixels (default 256) 54 | -i, --ssid string Wireless network name 55 | -v, --version version for wifiqr 56 | ``` 57 | 58 | ## Usage Example 59 | 60 | ```sh 61 | ./wifiqr --ssid some_ssid --key 1234 --output qr.png --size 128 62 | ``` 63 | 64 | ## License 65 | 66 | MIT 67 | -------------------------------------------------------------------------------- /cmd/wifiqr/.goreleaser.yml: -------------------------------------------------------------------------------- 1 | project_name: wifiqr 2 | builds: 3 | - main: . 4 | ldflags: 5 | - -s -w -X main.version={{.Version}} 6 | env: [CGO_ENABLED=0] 7 | goos: 8 | - linux 9 | - windows 10 | - darwin 11 | goarch: 12 | - amd64 13 | - arm64 14 | - 386 15 | archives: 16 | - name_template: >- 17 | {{ .ProjectName }}_{{ .Version }}_ 18 | {{- if eq .Os "darwin" }}macos 19 | {{- else }}{{ .Os }}{{ end }}_ 20 | {{- if eq .Arch "amd64" }}x86_64 21 | {{- else }}{{ .Arch }}{{ end }} 22 | format_overrides: 23 | - goos: windows 24 | format: zip -------------------------------------------------------------------------------- /cmd/wifiqr/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "os" 7 | "path/filepath" 8 | "strings" 9 | 10 | "github.com/manifoldco/promptui" 11 | "github.com/reugn/wifiqr" 12 | "github.com/skip2/go-qrcode" 13 | "github.com/spf13/cobra" 14 | ) 15 | 16 | const ( 17 | ssidDesc = "network name (SSID)" 18 | pskDesc = "network key (password)" 19 | ) 20 | 21 | var version = "develop" 22 | 23 | // generateCode creates a WiFi QR code. 24 | func generateCode(ssid, key string, encoding wifiqr.EncryptionProtocol, hidden bool) (*qrcode.QRCode, error) { 25 | q, err := wifiqr.InitCode( 26 | wifiqr.NewConfig(ssid, key, encoding, hidden), 27 | ) 28 | 29 | if err != nil { 30 | fmt.Fprintln(os.Stderr, "Error generating WiFi QR code:", err) 31 | } 32 | 33 | return q, err 34 | } 35 | 36 | // validateAndGetFilename adds the .png extension to the 37 | // filename if it doesn't already have one. 38 | func validateAndGetFilename(filename string) string { 39 | const pngExt = ".png" 40 | 41 | if filepath.Ext(filename) != pngExt { 42 | filename += pngExt 43 | } 44 | 45 | return filename 46 | } 47 | 48 | // saveCode saves the QR code to a file. 49 | func saveCode(q *qrcode.QRCode, filename string, size int) error { 50 | if err := q.WriteFile(size, validateAndGetFilename(filename)); err != nil { 51 | fmt.Fprintln(os.Stderr, "Error saving WiFi QR code:", err) 52 | return err 53 | } 54 | 55 | fmt.Println("WiFi QR code saved to " + filename + ".") 56 | 57 | return nil 58 | } 59 | 60 | // outputCode outputs the QR code to stdout or to a file. 61 | func outputCode(q *qrcode.QRCode, filename string, size int) error { 62 | if filename == "" { 63 | fmt.Println(q.ToSmallString(false)) 64 | return nil 65 | } 66 | 67 | return saveCode(q, filename, size) 68 | } 69 | 70 | // getInput gets user input using promptui. 71 | func getInput(prompt string, validate func(string) error) (string, error) { 72 | return (&promptui.Prompt{ 73 | Label: prompt, 74 | Validate: validate, 75 | }).Run() 76 | } 77 | 78 | // inputValidator is a generic function for getting user input if the value is empty. 79 | func inputValidator(value, prompt string, validate func(string) error) (string, error) { 80 | var err error 81 | 82 | if value == "" { 83 | value, err = getInput(prompt, validate) 84 | } 85 | 86 | return value, err 87 | } 88 | 89 | // validateSSID gets the SSID from the user if it is empty. 90 | func validateSSID(ssid string) (string, error) { 91 | return inputValidator(ssid, "Enter the "+ssidDesc, func(input string) error { 92 | if len(input) == 0 { 93 | return errors.New("empty") 94 | } 95 | 96 | // https://serverfault.com/questions/45439/what-is-the-maximum-length-of-a-wifi-access-points-ssid 97 | if len(input) > 32 { 98 | return errors.New("maximum length exceeded") 99 | } 100 | 101 | return nil 102 | }) 103 | } 104 | 105 | // validateKey gets the key from the user if it is empty. 106 | func validateKey(key string) (string, error) { 107 | return inputValidator(key, "Enter the "+pskDesc, func(input string) error { 108 | if len(input) == 0 { 109 | return errors.New("empty") 110 | } 111 | 112 | // no check for maximum length because it can get messy 113 | return nil 114 | }) 115 | } 116 | 117 | // validateEncryption gets the encryption protocol from the user 118 | // if the provided protocol is empty or not valid. 119 | func validateEncryption(protocol string) (wifiqr.EncryptionProtocol, error) { 120 | encProtocol, err := wifiqr.NewEncryptionProtocol(protocol) 121 | if err != nil { 122 | fmt.Println("Invalid encryption protocol.") 123 | } else { 124 | return encProtocol, nil 125 | } 126 | 127 | prompt := promptui.Select{ 128 | Label: "Select the encryption protocol type", 129 | Items: []string{ 130 | wifiqr.WPA2.String(), 131 | wifiqr.WPA.String(), 132 | wifiqr.WEP.String(), 133 | wifiqr.NONE.String(), 134 | }, 135 | } 136 | 137 | _, enc, _ := prompt.Run() 138 | 139 | return wifiqr.NewEncryptionProtocol(enc) 140 | } 141 | 142 | // process generates the QR code given the parameters and can be 143 | // considered to be a layer below that of the CLI. 144 | func process(ssid, protocolIn, output string, pixels int, key string, keySet bool, hidden bool) int { 145 | var err error 146 | 147 | ssid, err = validateSSID(ssid) 148 | if err != nil { 149 | return 1 150 | } 151 | 152 | protocol, err := validateEncryption(protocolIn) 153 | if err != nil { 154 | return 1 155 | } 156 | 157 | if protocol != wifiqr.NONE && !keySet { 158 | key, err = validateKey(key) 159 | if err != nil { 160 | return 1 161 | } 162 | } 163 | 164 | q, err := generateCode(ssid, key, protocol, hidden) 165 | if err != nil { 166 | return 1 167 | } 168 | 169 | if outputCode(q, output, pixels) != nil { 170 | return 1 171 | } 172 | 173 | return 0 174 | } 175 | 176 | // run processes the CLI arguments using Cobra then passes control 177 | // to the process function, using the CLI options as function 178 | // parameters. 179 | func run() int { 180 | const optionKey = "key" 181 | var ( 182 | ssid, key, protocolIn, output string 183 | pixels, exitVal int 184 | hidden, keySet bool 185 | ) 186 | 187 | rootCmd := &cobra.Command{ 188 | Use: "wifiqr", 189 | Short: "wifiqr is a WiFi QR code generator", 190 | Long: `wifiqr is a WiFi QR code generator 191 | 192 | It is used to create a QR code containing the login details such as 193 | the name, password, and encryption type. This Android and iOS 194 | compatible QR code can be scanned using Google Lens or other QR code 195 | reader to connect to the network. 196 | 197 | If the options necessary for creating the QR code are not given on 198 | the command line, the user will be prompted for the information.`, 199 | Version: version, 200 | } 201 | 202 | rootCmd.Run = func(cmd *cobra.Command, args []string) { 203 | keySet = rootCmd.Flags().Changed(optionKey) 204 | 205 | exitVal = process(ssid, protocolIn, output, pixels, key, keySet, hidden) 206 | } 207 | 208 | rootCmd.Flags().StringVarP(&ssid, "ssid", "i", "", "Wireless network name") 209 | rootCmd.Flags().StringVarP(&key, optionKey, "k", "", "Wireless password (pre-shared key / PSK)") 210 | rootCmd.Flags().StringVarP(&protocolIn, "protocol", "p", wifiqr.WPA2.String(), 211 | "Wireless network encryption protocol ("+ 212 | strings.Join([]string{ 213 | wifiqr.WPA2.String(), 214 | wifiqr.WPA.String(), 215 | wifiqr.WEP.String(), 216 | wifiqr.NONE.String(), 217 | }, ", ")+ 218 | ").") 219 | rootCmd.Flags().BoolVarP(&hidden, "hidden", "", false, "Hidden SSID") 220 | rootCmd.Flags().StringVarP(&output, "output", "o", "", "PNG file for output (default stdout)") 221 | rootCmd.Flags().IntVarP(&pixels, "size", "s", 256, "Image width and height in pixels") 222 | 223 | if err := rootCmd.Execute(); err != nil { 224 | fmt.Fprintln(os.Stderr, err) 225 | exitVal = 1 226 | } 227 | 228 | return exitVal 229 | } 230 | 231 | func main() { 232 | os.Exit(run()) 233 | } 234 | -------------------------------------------------------------------------------- /cmd/wifiqr/main_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "crypto/sha256" 5 | "math/rand" 6 | "strconv" 7 | "testing" 8 | 9 | "github.com/reugn/wifiqr" 10 | ) 11 | 12 | func byteString(b [32]byte) string { 13 | s := "[32]byte{ " 14 | l := len(b) - 1 15 | for i, x := range b { 16 | s += strconv.Itoa(int(x)) 17 | if i != l { 18 | s += ", " 19 | } 20 | } 21 | 22 | return s + " }" 23 | } 24 | 25 | func Test_generateCode(t *testing.T) { 26 | const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" 27 | 28 | b := make([]byte, 7100) 29 | for i := range b { 30 | b[i] = letterBytes[rand.Intn(len(letterBytes))] 31 | } 32 | longString := string(b) 33 | 34 | type args struct { 35 | ssid string 36 | key string 37 | encoding wifiqr.EncryptionProtocol 38 | hidden bool 39 | } 40 | tests := []struct { 41 | name string 42 | args args 43 | want [32]byte 44 | wantErr bool 45 | }{ 46 | { 47 | name: "encoder success", 48 | args: args{ 49 | ssid: "testssid", 50 | key: "testkeytestkey", 51 | encoding: wifiqr.WPA2, 52 | hidden: false, 53 | }, 54 | want: [32]byte{117, 113, 240, 31, 70, 131, 178, 237, 61, 56, 190, 135, 145, 86, 173, 81, 55 | 244, 78, 103, 173, 103, 188, 82, 70, 79, 180, 149, 217, 5, 113, 227, 25}, 56 | wantErr: false, 57 | }, 58 | { 59 | name: "encoder failure", 60 | args: args{ 61 | ssid: longString, 62 | key: "test", 63 | encoding: wifiqr.WPA2, 64 | hidden: false, 65 | }, 66 | wantErr: true, 67 | }, 68 | } 69 | for _, tt := range tests { 70 | t.Run(tt.name, func(t *testing.T) { 71 | got, err := generateCode(tt.args.ssid, tt.args.key, tt.args.encoding, tt.args.hidden) 72 | if (err != nil) != tt.wantErr { 73 | t.Errorf("generateCode() error = %v, wantErr %v", err, tt.wantErr) 74 | return 75 | } 76 | 77 | if !tt.wantErr { 78 | if data, err := got.PNG(512); err != nil { 79 | t.Errorf("generateCode() error generating png: %v", err) 80 | } else { 81 | hash := sha256.Sum256(data) 82 | if tt.want != hash { 83 | t.Errorf("generateCode() png data does not match wanted hash, got: %v, want %v", 84 | byteString(hash), byteString(tt.want)) 85 | } 86 | } 87 | } 88 | }) 89 | } 90 | } 91 | 92 | func Test_validateAndGetFileName(t *testing.T) { 93 | type args struct { 94 | filename string 95 | } 96 | tests := []struct { 97 | name string 98 | args args 99 | want string 100 | }{ 101 | { 102 | name: "no png suffix", 103 | args: args{ 104 | filename: "imagefilename", 105 | }, 106 | want: "imagefilename.png", 107 | }, 108 | { 109 | name: "with png suffix", 110 | args: args{ 111 | filename: "imagefilename.png", 112 | }, 113 | want: "imagefilename.png", 114 | }, 115 | } 116 | for _, tt := range tests { 117 | t.Run(tt.name, func(t *testing.T) { 118 | if got := validateAndGetFilename(tt.args.filename); got != tt.want { 119 | t.Errorf("validateAndGetFileName() = %v, want %v", got, tt.want) 120 | } 121 | }) 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /config.go: -------------------------------------------------------------------------------- 1 | package wifiqr 2 | 3 | import ( 4 | "errors" 5 | "strings" 6 | ) 7 | 8 | // EncryptionProtocol represents a WiFi encryption protocol. 9 | type EncryptionProtocol int 10 | 11 | const ( 12 | WPA2 EncryptionProtocol = iota 13 | WPA 14 | WEP 15 | NONE 16 | 17 | wpa2Str = "WPA2" 18 | wpaStr = "WPA" 19 | wepStr = "WEP" 20 | noneStr = "NONE" 21 | 22 | wpa2Code = wpa2Str 23 | wpaCode = wpaStr 24 | wepCode = wepStr 25 | noneCode = "nopass" 26 | ) 27 | 28 | // String returns the string representation of the EncryptionProtocol. 29 | func (ep EncryptionProtocol) String() string { 30 | switch ep { 31 | case WPA2: 32 | return wpa2Str 33 | case WPA: 34 | return wpaStr 35 | case WEP: 36 | return wepStr 37 | case NONE: 38 | return noneStr 39 | } 40 | return "" 41 | } 42 | 43 | // Code returns a string code for the EncryptionProtocol. 44 | func (ep EncryptionProtocol) Code() string { 45 | switch ep { 46 | case WPA2: 47 | return wpa2Code 48 | case WPA: 49 | return wpaCode 50 | case WEP: 51 | return wepCode 52 | case NONE: 53 | return noneCode 54 | } 55 | return "" 56 | } 57 | 58 | // NewEncryptionProtocol returns a new EncryptionProtocol from the specified string. 59 | func NewEncryptionProtocol(t string) (EncryptionProtocol, error) { 60 | switch strings.ToUpper(t) { 61 | case wpa2Str: 62 | return WPA2, nil 63 | case wpaStr: 64 | return WPA, nil 65 | case wepStr: 66 | return WEP, nil 67 | case noneStr, noneCode, "": 68 | return NONE, nil 69 | } 70 | return WPA2, errors.New("no such protocol") 71 | } 72 | 73 | // Config is the Wi-Fi network configuration parameters. 74 | type Config struct { 75 | // The Service Set Identifier (SSID) is the name of the wireless network. 76 | // It can be contained in the beacons sent out by APs, or it can be ‘hidden’ so that clients 77 | // who wish to associate must first know the name of the network. Early security guidance was 78 | // to hide the SSID of your network, but modern networking tools can detect the SSID by simply 79 | // watching for legitimate client association, as SSIDs are transmitted in cleartext. 80 | SSID string 81 | // A pre-shared key (PSK). 82 | Key string 83 | // The wireless network encryption protocol (WEP, WPA, WPA2). 84 | Encryption EncryptionProtocol 85 | // Defines if the SSID is ‘hidden’. 86 | Hidden bool 87 | } 88 | 89 | // NewConfig returns a new Config. 90 | func NewConfig(ssid string, key string, enc EncryptionProtocol, hidden bool) *Config { 91 | return &Config{ 92 | SSID: ssid, 93 | Key: key, 94 | Encryption: enc, 95 | Hidden: hidden, 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /config_test.go: -------------------------------------------------------------------------------- 1 | package wifiqr 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestNewEncryptionProtocol(t *testing.T) { 8 | type args struct { 9 | t string 10 | } 11 | tests := []struct { 12 | name string 13 | args args 14 | want EncryptionProtocol 15 | wantErr bool 16 | }{ 17 | { 18 | name: "WPA", 19 | args: args{ 20 | t: "WPA", 21 | }, 22 | want: WPA, 23 | wantErr: false, 24 | }, 25 | { 26 | name: "WPA2", 27 | args: args{ 28 | t: "WPA2", 29 | }, 30 | want: WPA2, 31 | wantErr: false, 32 | }, 33 | { 34 | name: "WEP", 35 | args: args{ 36 | t: "WEP", 37 | }, 38 | want: WEP, 39 | wantErr: false, 40 | }, 41 | { 42 | name: "NONE", 43 | args: args{ 44 | t: "NONE", 45 | }, 46 | want: NONE, 47 | wantErr: false, 48 | }, 49 | { 50 | name: "error", 51 | args: args{ 52 | t: "invalid", 53 | }, 54 | want: WPA2, 55 | wantErr: true, 56 | }, 57 | } 58 | for _, tt := range tests { 59 | t.Run(tt.name, func(t *testing.T) { 60 | got, err := NewEncryptionProtocol(tt.args.t) 61 | if (err != nil) != tt.wantErr { 62 | t.Errorf("NewEncryptionProtocol() error = %v, wantErr %v", err, tt.wantErr) 63 | return 64 | } 65 | if got != tt.want { 66 | t.Errorf("NewEncryptionProtocol() = %v, want %v", got, tt.want) 67 | } 68 | }) 69 | } 70 | } 71 | 72 | func TestEncryptionProtocol_String(t *testing.T) { 73 | tests := []struct { 74 | name string 75 | ep EncryptionProtocol 76 | want string 77 | }{ 78 | { 79 | name: "WPA", 80 | ep: WPA, 81 | want: "WPA", 82 | }, 83 | { 84 | name: "WPA2", 85 | ep: WPA2, 86 | want: "WPA2", 87 | }, 88 | { 89 | name: "WEP", 90 | ep: WEP, 91 | want: "WEP", 92 | }, 93 | { 94 | name: "NONE", 95 | ep: NONE, 96 | want: "NONE", 97 | }, 98 | { 99 | name: "NONE", 100 | ep: 99, 101 | want: "", 102 | }, 103 | } 104 | for _, tt := range tests { 105 | t.Run(tt.name, func(t *testing.T) { 106 | if got := tt.ep.String(); got != tt.want { 107 | t.Errorf("EncryptionProtocol.String() = %v, want %v", got, tt.want) 108 | } 109 | }) 110 | } 111 | } 112 | 113 | func TestEncryptionProtocol_Code(t *testing.T) { 114 | tests := []struct { 115 | name string 116 | ep EncryptionProtocol 117 | want string 118 | }{ 119 | { 120 | name: "WPA", 121 | ep: WPA, 122 | want: "WPA", 123 | }, 124 | { 125 | name: "WPA2", 126 | ep: WPA2, 127 | want: "WPA2", 128 | }, 129 | { 130 | name: "WEP", 131 | ep: WEP, 132 | want: "WEP", 133 | }, 134 | { 135 | name: "NONE", 136 | ep: NONE, 137 | want: "nopass", 138 | }, 139 | { 140 | name: "NONE", 141 | ep: 99, 142 | want: "", 143 | }} 144 | for _, tt := range tests { 145 | t.Run(tt.name, func(t *testing.T) { 146 | if got := tt.ep.Code(); got != tt.want { 147 | t.Errorf("EncryptionProtocol.Code() = %v, want %v", got, tt.want) 148 | } 149 | }) 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /docs/images/qr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reugn/wifiqr/8924ee0e34b95ffbbcfde6268ef5389ab9445b6f/docs/images/qr.png -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/reugn/wifiqr 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/manifoldco/promptui v0.9.0 7 | github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e 8 | github.com/spf13/cobra v1.8.0 9 | ) 10 | 11 | require ( 12 | github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e // indirect 13 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 14 | github.com/spf13/pflag v1.0.5 // indirect 15 | golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b // indirect 16 | ) 17 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE= 2 | github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= 3 | github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5OhCuC+XN+r/bBCmeuuJtjz+bCNIf8= 4 | github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= 5 | github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8= 6 | github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= 7 | github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 8 | github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= 9 | github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 10 | github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA= 11 | github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg= 12 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 13 | github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0= 14 | github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M= 15 | github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= 16 | github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= 17 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 18 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 19 | golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b h1:MQE+LT/ABUuuvEZ+YQAMSXindAdUh7slEmAkup74op4= 20 | golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 21 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 22 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 23 | -------------------------------------------------------------------------------- /wifiqr.go: -------------------------------------------------------------------------------- 1 | package wifiqr 2 | 3 | import ( 4 | "strconv" 5 | "strings" 6 | 7 | "github.com/skip2/go-qrcode" 8 | ) 9 | 10 | // There are several levels of error detection/recovery capacity. Higher levels 11 | // of error recovery are able to correct more errors, with the trade-off of 12 | // increased symbol size. 13 | var defaultRecoveryLevel qrcode.RecoveryLevel = qrcode.High 14 | 15 | // InitCode returns the qrcode.QRCode based on the configuration. 16 | func InitCode(config *Config) (*qrcode.QRCode, error) { 17 | return qrcode.New(buildSchema(config), defaultRecoveryLevel) 18 | } 19 | 20 | // escapeString escapes the special characters with a backslash. 21 | func escapeString(s string) string { 22 | // https://github.com/zxing/zxing/wiki/Barcode-Contents#wi-fi-network-config-android-ios-11 23 | 24 | for _, c := range []byte{'\\', ';', ',', '"', ':'} { 25 | s = strings.ReplaceAll(s, string(c), `\`+string(c)) 26 | } 27 | 28 | return s 29 | } 30 | 31 | // WIFI:S:My_SSID;T:WPA;P:key goes here;H:false; 32 | // ^ ^ ^ ^ ^ 33 | // | | | | +-- hidden SSID (true/false) 34 | // | | | +-- WPA key 35 | // | | +-- encryption type 36 | // | +-- ESSID 37 | // +-- code type 38 | func buildSchema(config *Config) string { 39 | return "WIFI:S:" + 40 | escapeString(config.SSID) + 41 | ";T:" + 42 | config.Encryption.Code() + 43 | ";P:" + 44 | escapeString(config.Key) + 45 | ";H:" + 46 | strconv.FormatBool(config.Hidden) + 47 | ";" 48 | } 49 | -------------------------------------------------------------------------------- /wifiqr_test.go: -------------------------------------------------------------------------------- 1 | package wifiqr 2 | 3 | import ( 4 | "hash/fnv" 5 | "reflect" 6 | "testing" 7 | ) 8 | 9 | func TestCode(t *testing.T) { 10 | config := NewConfig("ssid1", "1234", WPA2, false) 11 | qrCode, err := InitCode(config) 12 | if err != nil { 13 | t.Fatal(err) 14 | } 15 | assertEqual(t, hashCode(qrCode.ToString(false)), 550955445) 16 | } 17 | 18 | func hashCode(s string) int { 19 | h := fnv.New32a() 20 | h.Write([]byte(s)) 21 | return int(h.Sum32()) 22 | } 23 | 24 | func assertEqual(t *testing.T, a interface{}, b interface{}) { 25 | if !reflect.DeepEqual(a, b) { 26 | t.Fatalf("%v != %v", a, b) 27 | } 28 | } 29 | 30 | func Test_escapeString(t *testing.T) { 31 | type args struct { 32 | in string 33 | } 34 | tests := []struct { 35 | name string 36 | args args 37 | want string 38 | }{ 39 | { 40 | name: "all escapes", 41 | args: args{ 42 | in: `abc\;,":xyz`, 43 | }, 44 | want: `abc\\\;\,\"\:xyz`, 45 | }, 46 | } 47 | for _, tt := range tests { 48 | t.Run(tt.name, func(t *testing.T) { 49 | if got := escapeString(tt.args.in); got != tt.want { 50 | t.Errorf("escapeString() = %v, want %v", got, tt.want) 51 | } 52 | }) 53 | } 54 | } 55 | 56 | func Test_buildSchema(t *testing.T) { 57 | type args struct { 58 | config *Config 59 | } 60 | tests := []struct { 61 | name string 62 | args args 63 | want string 64 | }{ 65 | { 66 | name: "no encryption protocol", 67 | args: args{ 68 | config: &Config{ 69 | SSID: "ssid1", 70 | Encryption: NONE, 71 | }, 72 | }, 73 | want: `WIFI:S:ssid1;T:nopass;P:;H:false;`, 74 | }, 75 | { 76 | name: "WEP encryption protocol", 77 | args: args{ 78 | config: &Config{ 79 | SSID: "ssid1", 80 | Key: "key1", 81 | Encryption: WEP, 82 | }, 83 | }, 84 | want: `WIFI:S:ssid1;T:WEP;P:key1;H:false;`, 85 | }, 86 | { 87 | name: "WPA encryption protocol", 88 | args: args{ 89 | config: &Config{ 90 | SSID: "ssid1", 91 | Key: "key1", 92 | Encryption: WPA, 93 | }, 94 | }, 95 | want: `WIFI:S:ssid1;T:WPA;P:key1;H:false;`, 96 | }, 97 | { 98 | name: "WPA2 encryption protocol", 99 | args: args{ 100 | config: &Config{ 101 | SSID: "ssid1", 102 | Key: "key1", 103 | Encryption: WPA2, 104 | }, 105 | }, 106 | want: `WIFI:S:ssid1;T:WPA2;P:key1;H:false;`, 107 | }, 108 | { 109 | name: "WPA2 encryption protocol, hidden", 110 | args: args{ 111 | config: &Config{ 112 | SSID: "ssid1", 113 | Key: "key1", 114 | Encryption: WPA2, 115 | Hidden: true, 116 | }, 117 | }, 118 | want: `WIFI:S:ssid1;T:WPA2;P:key1;H:true;`, 119 | }, 120 | { 121 | name: "escaped characters, WPA2 encryption protocol", 122 | args: args{ 123 | config: &Config{ 124 | SSID: `abc\;,":xyz`, 125 | Key: `xyz\;,":abc`, 126 | Encryption: WPA2, 127 | Hidden: false, 128 | }, 129 | }, 130 | want: `WIFI:S:abc\\\;\,\"\:xyz;T:WPA2;P:xyz\\\;\,\"\:abc;H:false;`, 131 | }, 132 | } 133 | for _, tt := range tests { 134 | t.Run(tt.name, func(t *testing.T) { 135 | if got := buildSchema(tt.args.config); got != tt.want { 136 | t.Errorf("buildSchema() = %v, want %v", got, tt.want) 137 | } 138 | }) 139 | } 140 | } 141 | --------------------------------------------------------------------------------