Last active
March 9, 2024 18:16
-
-
Save heatxsink/db53ddeced344b4bde32172551a96e87 to your computer and use it in GitHub Desktop.
SMART Health Card (COVID19 Vaccine) QR Code processing / decoding in golang
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Download your SMART Health Card QR code image somewhere on your filesystem. | |
// I'm in California, so I went here: https://myvaccinerecord.cdph.ca.gov/ | |
// | |
// Some links I found useful when hacking this up ... | |
// - https://www.reddit.com/r/Quebec/comments/ndz2uz/how_the_covid_vaccination_qr_code_works_and_what/ | |
// - https://github.com/dvci/health-cards-walkthrough/blob/main/SMART%20Health%20Cards.ipynb | |
// - https://smarthealth.cards | |
// - https://github.com/fproulx/shc-covid19-decoder | |
// - https://github.com/smart-on-fhir/health-cards/blob/main/docs/index.md#every-health-card-can-be-embedded-in-a-qr-code | |
// - https://github.com/smart-on-fhir/health-cards/blob/main/docs/index.md#encoding-chunks-as-qr-codes | |
package main | |
import ( | |
"bytes" | |
"compress/flate" | |
"encoding/base64" | |
"encoding/json" | |
"flag" | |
"fmt" | |
"image" | |
_ "image/jpeg" | |
_ "image/png" | |
"io" | |
"io/ioutil" | |
"os" | |
"strconv" | |
"strings" | |
"github.com/makiuchi-d/gozxing" | |
"github.com/makiuchi-d/gozxing/qrcode" | |
"gopkg.in/square/go-jose.v2" | |
) | |
var ( | |
filenameOption string | |
) | |
func init() { | |
flag.StringVar(&filenameOption, "f", "", "path to QR code filename") | |
flag.Usage = usage | |
flag.Parse() | |
} | |
func usage() { | |
flag.PrintDefaults() | |
} | |
// More info on SMART Health Cards over here -> https://smarthealth.cards | |
type HealthCard struct { | |
Iss string `json:"iss,omitempty"` | |
Nbf float64 `json:"nbf,omitempty"` | |
Vc struct { | |
CredentialSubject struct { | |
FhirBundle struct { | |
Entry []struct { | |
FullUrl string `json:"fullUrl,omitempty"` | |
Resource struct { | |
BirthDate string `json:"birthDate,omitempty"` | |
Name []struct { | |
Family string `json:"family,omitempty"` | |
Given []string `json:"given,omitempty"` | |
} `json:"name,omitempty"` | |
ResourceType string `json:"resourceType,omitempty"` | |
} `json:"resource,omitempty"` | |
} `json:"entry,omitempty"` | |
ResourceType string `json:"resourceType,omitempty"` | |
Type string `json:"type,omitempty"` | |
} `json:"fhirBundle,omitempty"` | |
FhirVersion string `json:"fhirVersion,omitempty"` | |
} `json:"credentialSubject,omitempty"` | |
Type []string `json:"type,omitempty"` | |
} `json:"vc,omitempty"` | |
} | |
func decodeQRCode(path string) (*gozxing.Result, error) { | |
d, err := ioutil.ReadFile(path) | |
if err != nil { | |
return nil, err | |
} | |
img, _, err := image.Decode(bytes.NewReader(d)) | |
if err != nil { | |
return nil, err | |
} | |
bmp, err := gozxing.NewBinaryBitmapFromImage(img) | |
if err != nil { | |
return nil, err | |
} | |
qrReader := qrcode.NewQRCodeReader() | |
return qrReader.Decode(bmp, nil) | |
} | |
// This helped me understand how to decodeJWS ... | |
// https://github.com/fproulx/shc-covid19-decoder/blob/main/src/shc.js | |
func decodeJWS(text string) ([]byte, error) { | |
n := strings.TrimPrefix(text, "shc:/") | |
var rr []rune | |
for i := 0; i < len(n); i = i + 2 { | |
nn, err := strconv.Atoi(fmt.Sprintf("%c%c", n[i], n[i+1])) | |
if err != nil { | |
return nil, err | |
} | |
rr = append(rr, rune(nn+45)) | |
} | |
j, err := jose.ParseSigned(string(rr)) | |
if err != nil { | |
return nil, err | |
} | |
x, err := j.CompactSerialize() | |
if err != nil { | |
return nil, err | |
} | |
tokens := strings.Split(x, ".") | |
payload, err := base64.RawURLEncoding.DecodeString(tokens[1]) | |
if err != nil { | |
return nil, err | |
} | |
b := bytes.NewReader(payload) | |
f := flate.NewReader(b) | |
bb := new(bytes.Buffer) | |
_, err = io.Copy(bb, f) | |
if err != nil { | |
return nil, err | |
} | |
f.Close() | |
return bb.Bytes(), nil | |
} | |
func fileExists(filename string) bool { | |
info, err := os.Stat(filename) | |
if os.IsNotExist(err) { | |
return false | |
} | |
return !info.IsDir() | |
} | |
func main() { | |
if filenameOption == "" { | |
fmt.Println("Filename is required.") | |
flag.Usage() | |
os.Exit(1) | |
} | |
if !fileExists(filenameOption) { | |
fmt.Printf("The filename '%s' does not exist.\n", filenameOption) | |
flag.Usage() | |
os.Exit(1) | |
} | |
r, err := decodeQRCode(filenameOption) | |
if err != nil { | |
fmt.Printf("%#v", err) | |
} | |
b, err := decodeJWS(r.GetText()) | |
if err != nil { | |
fmt.Printf("%#v", err) | |
} | |
fmt.Println(string(b)) | |
var hc HealthCard | |
err = json.Unmarshal(b, &hc) | |
if err != nil { | |
fmt.Printf("%#v", err) | |
} | |
fmt.Println(hc.Iss, hc.Nbf, hc.Vc.Type) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment