├── example.png ├── .github ├── dependabot.yml └── workflows │ ├── test.yml │ └── static-analysis.yml ├── go.mod ├── go.sum ├── LICENSE.md ├── cmd └── wifiqr │ └── main.go ├── README.md ├── wifiqr_test.go └── wifiqr.go /example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdlayher/wifiqr/HEAD/example.png -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "gomod" 4 | directory: "/" 5 | schedule: 6 | interval: "monthly" 7 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/mdlayher/wifiqr 2 | 3 | go 1.20 4 | 5 | require ( 6 | github.com/google/go-cmp v0.6.0 7 | github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e 8 | golang.org/x/term v0.21.0 9 | ) 10 | 11 | require golang.org/x/sys v0.21.0 // indirect 12 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | push: 5 | branches: 6 | - "*" 7 | pull_request: 8 | branches: 9 | - "*" 10 | 11 | jobs: 12 | build: 13 | strategy: 14 | fail-fast: false 15 | matrix: 16 | go-version: ["1.20"] 17 | os: [ubuntu-latest] 18 | runs-on: ${{ matrix.os }} 19 | 20 | steps: 21 | - name: Set up Go 22 | uses: actions/setup-go@v3 23 | with: 24 | go-version: ${{ matrix.go-version }} 25 | id: go 26 | 27 | - name: Check out code into the Go module directory 28 | uses: actions/checkout@v3 29 | 30 | - name: Run tests 31 | run: go test -race ./... 32 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 2 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 3 | github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0= 4 | github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M= 5 | golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= 6 | golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 7 | golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA= 8 | golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0= 9 | -------------------------------------------------------------------------------- /.github/workflows/static-analysis.yml: -------------------------------------------------------------------------------- 1 | name: Static Analysis 2 | 3 | on: 4 | push: 5 | branches: 6 | - "*" 7 | pull_request: 8 | branches: 9 | - "*" 10 | 11 | jobs: 12 | build: 13 | strategy: 14 | matrix: 15 | go-version: ["1.20"] 16 | runs-on: ubuntu-latest 17 | 18 | steps: 19 | - name: Set up Go 20 | uses: actions/setup-go@v3 21 | with: 22 | go-version: ${{ matrix.go-version }} 23 | id: go 24 | 25 | - name: Check out code into the Go module directory 26 | uses: actions/checkout@v3 27 | 28 | - name: Install staticcheck 29 | run: go install honnef.co/go/tools/cmd/staticcheck@latest 30 | 31 | - name: Print staticcheck version 32 | run: staticcheck -version 33 | 34 | - name: Run staticcheck 35 | run: staticcheck ./... 36 | 37 | - name: Run go vet 38 | run: go vet ./... 39 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | Copyright (C) 2023 Matt Layher 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /cmd/wifiqr/main.go: -------------------------------------------------------------------------------- 1 | // Command wifiqr generates a WiFi QR code image. 2 | package main 3 | 4 | import ( 5 | "flag" 6 | "fmt" 7 | "image/png" 8 | "io" 9 | "log" 10 | "os" 11 | 12 | "github.com/mdlayher/wifiqr" 13 | "golang.org/x/term" 14 | ) 15 | 16 | func main() { 17 | var ( 18 | sFlag = flag.String("s", "Example", "SSID, or WiFi network name") 19 | pFlag = flag.String("p", "thisisanexample", "WiFi network password") 20 | ) 21 | flag.Parse() 22 | 23 | cfg := wifiqr.Config{ 24 | Authentication: wifiqr.WPA, 25 | SSID: *sFlag, 26 | Password: *pFlag, 27 | } 28 | 29 | img, err := wifiqr.New(cfg) 30 | if err != nil { 31 | log.Fatalf("failed to create QR code: %v", err) 32 | } 33 | 34 | if term.IsTerminal(int(os.Stdout.Fd())) { 35 | fmt.Printf("SSID: %q, password: %q\n\n", cfg.SSID, cfg.Password) 36 | 37 | if _, err := io.WriteString(os.Stdout, img.String()); err != nil { 38 | log.Fatalf("failed to encode QR code for terminal: %v", err) 39 | } 40 | 41 | return 42 | } 43 | 44 | if err := png.Encode(os.Stdout, img.Image()); err != nil { 45 | log.Fatalf("failed to encode PNG: %v", err) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # wifiqr [![Test Status](https://github.com/mdlayher/wifiqr/workflows/Test/badge.svg)](https://github.com/mdlayher/wifiqr/actions) [![Go Reference](https://pkg.go.dev/badge/github.com/mdlayher/wifiqr.svg)](https://pkg.go.dev/github.com/mdlayher/wifiqr) [![Go Report Card](https://goreportcard.com/badge/github.com/mdlayher/wifiqr)](https://goreportcard.com/report/github.com/mdlayher/wifiqr) 2 | 3 | Package `wifiqr` implements support for generating WiFi QR codes. MIT Licensed. 4 | 5 | ## Example 6 | 7 | Generate a QR code image and redirect stdout to create a PNG file. 8 | 9 | ``` 10 | $ go run cmd/wifiqr/main.go > example.png 11 | ``` 12 | 13 | This produces: 14 | 15 | ![example](https://github.com/mdlayher/wifiqr/assets/1926905/6f46e6d1-a147-4d1a-8afb-0bd1e38034a7) 16 | 17 | Alternatively, if stdout is a terminal (and not redirected) you can display the 18 | QR code directly. 19 | 20 | ``` 21 | $ go run ./cmd/wifiqr/main.go 22 | SSID: "Example", password: "thisisanexample" 23 | 24 | █████████████████████████████████████ 25 | █████████████████████████████████████ 26 | ████ ▄▄▄▄▄ ██ ▀ ▀▀▄ ▄▀▄█ █ ▄▄▄▄▄ ████ 27 | ████ █ █ █ ▄▀ █ ▄▀▄▄ ▄▄█ █ █ ████ 28 | ████ █▄▄▄█ █ █ ▀█▀▄▄▄▀▄ ▀█ █▄▄▄█ ████ 29 | ████▄▄▄▄▄▄▄█ ▀▄█▄▀ █ ▀ ▀▄█▄▄▄▄▄▄▄████ 30 | ████ ▀▄ ▄ ▄█▀▄█ ▄▄▀ ██▄███▄ ▄█▀████ 31 | ████ ▄▄▄█▄▄▄▀▀ ▀▀██▄ ▄██ █▀▀ ▀▄█ ████ 32 | ████ █▀ █▀▄▄▄ ▄ ▄▀█ ▄▄▀▄ ▄██ ████ 33 | ████▀███▀▀▄▀▄▀▀▄▄ ▄█▀ ▀ ▄▀▀▄▀▀ ██████ 34 | ████▀▀ ▀▄▀▄█▀ ████▄ ▄█▄█ ▄█▄▀▀▄▀▀████ 35 | ████ █ ██▀▄▄▄▀ ▄▀█▀▄▀▄█▀▀▀▀ ▀█▄▀████ 36 | ████▄██▄▄█▄█▀▄█ ▀▀▀ █▄▄█ ▄▄▄ ▀ ▄▄████ 37 | ████ ▄▄▄▄▄ █▀█▄▀▄ ▄█ ▄▀▀ █▄█ ▄██████ 38 | ████ █ █ █ ▄▀██▄ █▄▄ ▄ ▄██▀█████ 39 | ████ █▄▄▄█ █▄▄█▄▀▀ ▄▀ ▀▄███▀█ ▀ ▀████ 40 | ████▄▄▄▄▄▄▄█▄▄▄▄▄██▄█▄████▄█▄█▄▄█████ 41 | █████████████████████████████████████ 42 | ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ 43 | ``` 44 | -------------------------------------------------------------------------------- /wifiqr_test.go: -------------------------------------------------------------------------------- 1 | package wifiqr 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/google/go-cmp/cmp" 7 | "github.com/skip2/go-qrcode" 8 | ) 9 | 10 | func TestImageString(t *testing.T) { 11 | const want = `█████████████████████████████████████ 12 | █████████████████████████████████████ 13 | ████ ▄▄▄▄▄ ██ ▀ ▀▀▄ ▄▀▄█ █ ▄▄▄▄▄ ████ 14 | ████ █ █ █ ▄▀ █ ▄▀▄▄ ▄▄█ █ █ ████ 15 | ████ █▄▄▄█ █ █ ▀█▀▄▄▄▀▄ ▀█ █▄▄▄█ ████ 16 | ████▄▄▄▄▄▄▄█ ▀▄█▄▀ █ ▀ ▀▄█▄▄▄▄▄▄▄████ 17 | ████ ▀▄ ▄ ▄█▀▄█ ▄▄▀ ██▄███▄ ▄█▀████ 18 | ████ ▄▄▄█▄▄▄▀▀ ▀▀██▄ ▄██ █▀▀ ▀▄█ ████ 19 | ████ █▀ █▀▄▄▄ ▄ ▄▀█ ▄▄▀▄ ▄██ ████ 20 | ████▀███▀▀▄▀▄▀▀▄▄ ▄█▀ ▀ ▄▀▀▄▀▀ ██████ 21 | ████▀▀ ▀▄▀▄█▀ ████▄ ▄█▄█ ▄█▄▀▀▄▀▀████ 22 | ████ █ ██▀▄▄▄▀ ▄▀█▀▄▀▄█▀▀▀▀ ▀█▄▀████ 23 | ████▄██▄▄█▄█▀▄█ ▀▀▀ █▄▄█ ▄▄▄ ▀ ▄▄████ 24 | ████ ▄▄▄▄▄ █▀█▄▀▄ ▄█ ▄▀▀ █▄█ ▄██████ 25 | ████ █ █ █ ▄▀██▄ █▄▄ ▄ ▄██▀█████ 26 | ████ █▄▄▄█ █▄▄█▄▀▀ ▄▀ ▀▄███▀█ ▀ ▀████ 27 | ████▄▄▄▄▄▄▄█▄▄▄▄▄██▄█▄████▄█▄█▄▄█████ 28 | █████████████████████████████████████ 29 | ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ 30 | ` 31 | 32 | img, err := New(Config{ 33 | Authentication: WPA, 34 | SSID: "Example", 35 | Password: "thisisanexample", 36 | }) 37 | if err != nil { 38 | t.Fatalf("failed to create image: %v", err) 39 | } 40 | 41 | // Display to terminal for manual verification with a phone. 42 | got := img.String() 43 | t.Logf("\n%s", got) 44 | 45 | if diff := cmp.Diff(want, got); diff != "" { 46 | t.Fatalf("unexpected QR code string (-want +got):\n%s", diff) 47 | } 48 | } 49 | 50 | func TestConfig_encode(t *testing.T) { 51 | tests := []struct { 52 | name string 53 | cfg Config 54 | str string 55 | ok bool 56 | }{ 57 | { 58 | name: "no SSID", 59 | cfg: Config{SSID: ""}, 60 | }, 61 | { 62 | name: "bad Authentication", 63 | cfg: Config{Authentication: -1}, 64 | }, 65 | { 66 | name: "None with password", 67 | cfg: Config{Password: "xxx"}, 68 | }, 69 | { 70 | name: "WEP no password", 71 | cfg: Config{Authentication: WEP}, 72 | }, 73 | { 74 | name: "WPA no password", 75 | cfg: Config{Authentication: WPA}, 76 | }, 77 | { 78 | name: "ok None", 79 | cfg: Config{SSID: "Foo"}, 80 | str: "WIFI:T:;S:Foo;;", 81 | ok: true, 82 | }, 83 | { 84 | name: "ok WEP", 85 | cfg: Config{ 86 | Authentication: WEP, 87 | SSID: "Bar", 88 | Password: "abc", 89 | }, 90 | str: "WIFI:T:WEP;S:Bar;P:abc;;", 91 | ok: true, 92 | }, 93 | { 94 | name: "ok WPA", 95 | cfg: Config{ 96 | Authentication: WPA, 97 | SSID: "Baz", 98 | Password: "def", 99 | }, 100 | str: "WIFI:T:WPA;S:Baz;P:def;;", 101 | ok: true, 102 | }, 103 | { 104 | name: "ok hidden", 105 | cfg: Config{ 106 | Authentication: WPA, 107 | SSID: "Qux", 108 | Password: "ghi", 109 | Hidden: true, 110 | }, 111 | str: "WIFI:T:WPA;S:Qux;P:ghi;H:true;;", 112 | ok: true, 113 | }, 114 | } 115 | 116 | for _, tt := range tests { 117 | t.Run(tt.name, func(t *testing.T) { 118 | str, err := tt.cfg.encode() 119 | if tt.ok && err != nil { 120 | t.Fatalf("failed to encode: %v", err) 121 | } 122 | if !tt.ok && err == nil { 123 | t.Fatal("expected an error, but none occurred") 124 | } 125 | if err != nil { 126 | t.Logf("err: %v", err) 127 | return 128 | } 129 | 130 | if diff := cmp.Diff(tt.str, str); diff != "" { 131 | t.Fatalf("unexpected encoded string (-want +got):\n%s", diff) 132 | } 133 | }) 134 | } 135 | } 136 | 137 | func TestRecoveryLevel_convert(t *testing.T) { 138 | tests := []struct { 139 | name string 140 | in RecoveryLevel 141 | out qrcode.RecoveryLevel 142 | }{ 143 | { 144 | name: "zero", 145 | out: qrcode.Medium, 146 | }, 147 | { 148 | name: "unhandled", 149 | in: 100, 150 | out: qrcode.Medium, 151 | }, 152 | { 153 | name: "low", 154 | in: Low, 155 | out: qrcode.Low, 156 | }, 157 | { 158 | name: "medium", 159 | in: Medium, 160 | out: qrcode.Medium, 161 | }, 162 | { 163 | name: "high", 164 | in: High, 165 | out: qrcode.High, 166 | }, 167 | { 168 | name: "highest", 169 | in: Highest, 170 | out: qrcode.Highest, 171 | }, 172 | } 173 | 174 | for _, tt := range tests { 175 | t.Run(tt.name, func(t *testing.T) { 176 | if diff := cmp.Diff(tt.out, tt.in.convert()); diff != "" { 177 | t.Fatalf("unexpected recovery level (-want +got):\n%s", diff) 178 | } 179 | }) 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /wifiqr.go: -------------------------------------------------------------------------------- 1 | // Package wifiqr implements support for generating WiFi QR codes. 2 | package wifiqr 3 | 4 | import ( 5 | "errors" 6 | "fmt" 7 | "image" 8 | "strings" 9 | 10 | qrcode "github.com/skip2/go-qrcode" 11 | ) 12 | 13 | // An Image is a WiFi QR code image which may be encoded in a variety of formats 14 | // for display. 15 | type Image struct{ qr *qrcode.QRCode } 16 | 17 | // New generates an image containing a WiFi QR code using the parameters defined 18 | // in Config. See the documentation of Config for details. 19 | func New(cfg Config) (*Image, error) { 20 | s, err := cfg.encode() 21 | if err != nil { 22 | return nil, err 23 | } 24 | 25 | qr, err := qrcode.New(s, cfg.RecoveryLevel.convert()) 26 | if err != nil { 27 | return nil, err 28 | } 29 | 30 | return &Image{qr: qr}, nil 31 | } 32 | 33 | // Image returns an image which may be encoded for external use. 34 | func (i *Image) Image() image.Image { 35 | // TODO(mdlayher): make dimensions configurable. 36 | return i.qr.Image(-10) 37 | } 38 | 39 | // String returns a Unicode string for an Image, suitable for display in a 40 | // terminal. 41 | func (i *Image) String() string { return i.qr.ToSmallString(false) } 42 | 43 | // Authentication defines the type of WiFi authentication used by a network. 44 | type Authentication int 45 | 46 | // Possible Authentication values. 47 | const ( 48 | None Authentication = iota 49 | WEP 50 | WPA 51 | ) 52 | 53 | // RecoveryLevel defines the QR code recovery and error detection level. 54 | type RecoveryLevel int 55 | 56 | // Possible RecoveryLevel values. Medium is a good default for most 57 | // applications. 58 | const ( 59 | Medium RecoveryLevel = iota 60 | Low 61 | High 62 | Highest 63 | ) 64 | 65 | // convert converts a RecoveryLevel to a qrcode.RecoveryLevel. 66 | func (rl RecoveryLevel) convert() qrcode.RecoveryLevel { 67 | // This conversion exists because the zero value for qrcode.RecoveryLevel is 68 | // "low" while we'd like medium to be the default instead, per the docs 69 | // stating it is a reasonable default. 70 | switch rl { 71 | case Low: 72 | return qrcode.Low 73 | case High: 74 | return qrcode.High 75 | case Highest: 76 | return qrcode.Highest 77 | case Medium: 78 | fallthrough 79 | default: 80 | // Medium or unspecified value. 81 | return qrcode.Medium 82 | } 83 | } 84 | 85 | // A Config defines the parameters for generating a WiFi QR code. 86 | type Config struct { 87 | // Authentication specifies the type of WiFi authentication used by a 88 | // network. The zero value is "None", meaning an open network. 89 | Authentication Authentication 90 | 91 | // SSID and Password define the WiFi network name and password, 92 | // respectively. 93 | // 94 | // SSID is required and an error will be returned if it is unset. 95 | // 96 | // Password must be set for WEP or WPA authentication. It must be empty if 97 | // Authentication is set to None. 98 | SSID, Password string 99 | 100 | // Hidden defines whether the WiFi network is hidden. 101 | Hidden bool 102 | 103 | // RecoveryLevel specifies the QR code recovery and error detection level. 104 | // If unset, Medium is used as the default. 105 | RecoveryLevel RecoveryLevel 106 | } 107 | 108 | // A kv holds a key/value string pair used to generate WiFi QR code values. 109 | type kv struct{ Key, Value string } 110 | 111 | // authKV generates the kv for the WiFi authentication type. 112 | func (c Config) authKV() (kv, error) { 113 | var v string 114 | switch c.Authentication { 115 | case None: 116 | // None has no value, but must also have no Password set. 117 | if c.Password != "" { 118 | return kv{}, errors.New("cannot set a password with no authentication type") 119 | } 120 | // WEP and WPA require passwords. 121 | case WEP: 122 | if c.Password == "" { 123 | return kv{}, errors.New("a password must be set for WEP authentication") 124 | } 125 | 126 | v = "WEP" 127 | case WPA: 128 | if c.Password == "" { 129 | return kv{}, errors.New("a password must be set for WPA authentication") 130 | } 131 | 132 | v = "WPA" 133 | default: 134 | return kv{}, errors.New("invalid authentication type") 135 | } 136 | 137 | return kv{Key: "T", Value: v}, nil 138 | } 139 | 140 | // encode encodes a Config as text suitable for generating a WiFi QR code. For 141 | // documentation on the text format, see: 142 | // https://www.qr-code-generator.com/solutions/wifi-qr-code/. 143 | func (c Config) encode() (string, error) { 144 | // All configs set authentication type and SSID. 145 | auth, err := c.authKV() 146 | if err != nil { 147 | return "", err 148 | } 149 | 150 | if c.SSID == "" { 151 | return "", errors.New("no SSID is set") 152 | } 153 | 154 | kvs := []kv{auth, {Key: "S", Value: c.SSID}} 155 | 156 | // Password and hidden are optional depending on the network. 157 | if c.Password != "" { 158 | kvs = append(kvs, kv{Key: "P", Value: c.Password}) 159 | } 160 | if c.Hidden { 161 | kvs = append(kvs, kv{Key: "H", Value: "true"}) 162 | } 163 | 164 | // Combine each key/value pair with a colon and semicolon terminator. 165 | var sb strings.Builder 166 | for _, kv := range kvs { 167 | fmt.Fprintf(&sb, "%s:%s;", kv.Key, kv.Value) 168 | } 169 | 170 | return fmt.Sprintf("WIFI:%s;", sb.String()), nil 171 | } 172 | --------------------------------------------------------------------------------