195 lines
4.5 KiB
Go
195 lines
4.5 KiB
Go
// Copyright (C) 2017 MichaĆ Matczuk
|
|
// Use of this source code is governed by a BSD-style
|
|
// license that can be found in the LICENSE file.
|
|
|
|
package id
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/sha256"
|
|
"crypto/subtle"
|
|
"encoding/base32"
|
|
"errors"
|
|
"fmt"
|
|
"regexp"
|
|
"strings"
|
|
|
|
"github.com/calmh/luhn"
|
|
)
|
|
|
|
// ID is the type representing a generated ID.
|
|
type ID [32]byte
|
|
|
|
// New generates a new ID from the given input bytes.
|
|
func New(data []byte) ID {
|
|
var id ID
|
|
|
|
hasher := sha256.New()
|
|
hasher.Write(data)
|
|
hasher.Sum(id[:0])
|
|
|
|
return id
|
|
}
|
|
|
|
// NewFromString creates a new ID from the given string.
|
|
func NewFromString(s string) ID {
|
|
return New([]byte(s))
|
|
}
|
|
|
|
// NewFromBytes creates a new ID with the value of the given byte slice. The
|
|
// given byte slice must be 32 bytes long (the same length as ID), or this
|
|
// function will panic.
|
|
func NewFromBytes(b []byte) ID {
|
|
var id ID
|
|
|
|
if len(b) != len(id) {
|
|
panic("invalid slice length for id")
|
|
}
|
|
|
|
copy(id[0:], b)
|
|
return id
|
|
}
|
|
|
|
// String returns the canonical representation of the ID.
|
|
func (i ID) String() string {
|
|
ss := base32.StdEncoding.EncodeToString(i[:])
|
|
ss = strings.Trim(ss, "=")
|
|
|
|
// Add a Luhn check 'digit' for the ID.
|
|
ss, err := luhnify(ss)
|
|
if err != nil {
|
|
// Should never happen
|
|
panic(err)
|
|
}
|
|
|
|
// Return the given ID as chunks.
|
|
ss = chunkify(ss)
|
|
|
|
return ss
|
|
}
|
|
|
|
// Compares the two given IDs. Note that this function is NOT SAFE AGAINST
|
|
// TIMING ATTACKS. If you are simply checking for equality, please use the
|
|
// Equals function, which is.
|
|
func (i ID) Compare(other ID) int {
|
|
return bytes.Compare(i[:], other[:])
|
|
}
|
|
|
|
// Checks the two given IDs for equality. This function uses a constant-time
|
|
// comparison algorithm to prevent timing attacks.
|
|
func (i ID) Equals(other ID) bool {
|
|
return subtle.ConstantTimeCompare(i[:], other[:]) == 1
|
|
}
|
|
|
|
// Implements the `TextMarshaler` interface from the encoding package.
|
|
func (i *ID) MarshalText() ([]byte, error) {
|
|
return []byte(i.String()), nil
|
|
}
|
|
|
|
// Implements the `TextUnmarshaler` interface from the encoding package.
|
|
func (i *ID) UnmarshalText(bs []byte) (err error) {
|
|
// Convert to the canonical encoding - uppercase, no '=', no chunks, and
|
|
// with any potential typos fixed.
|
|
id := string(bs)
|
|
id = strings.Trim(id, "=")
|
|
id = strings.ToUpper(id)
|
|
id = untypeoify(id)
|
|
id = unchunkify(id)
|
|
|
|
if len(id) != 56 {
|
|
return errors.New("device ID invalid: incorrect length")
|
|
}
|
|
|
|
// Remove & verify Luhn check digits
|
|
id, err = unluhnify(id)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Base32 decode
|
|
dec, err := base32.StdEncoding.DecodeString(id + "====")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Done!
|
|
copy(i[:], dec)
|
|
return nil
|
|
}
|
|
|
|
// Add Luhn check digits to a string, returning the new one.
|
|
func luhnify(s string) (string, error) {
|
|
if len(s) != 52 {
|
|
panic("unsupported string length")
|
|
}
|
|
|
|
// Split the string into chunks of length 13, and add a Luhn check digit to
|
|
// each one.
|
|
res := make([]string, 0, 4)
|
|
for i := 0; i < 4; i++ {
|
|
chunk := s[i*13 : (i+1)*13]
|
|
|
|
l, err := luhn.Base32.Generate(chunk)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
res = append(res, fmt.Sprintf("%s%c", chunk, l))
|
|
}
|
|
|
|
return res[0] + res[1] + res[2] + res[3], nil
|
|
}
|
|
|
|
// Remove Luhn check digits from the given string, validating that they are
|
|
// correct.
|
|
func unluhnify(s string) (string, error) {
|
|
if len(s) != 56 {
|
|
return "", fmt.Errorf("unsupported string length %d", len(s))
|
|
}
|
|
|
|
res := make([]string, 0, 4)
|
|
for i := 0; i < 4; i++ {
|
|
// 13 characters, plus the Luhn digit.
|
|
chunk := s[i*14 : (i+1)*14]
|
|
|
|
// Get the expected check digit.
|
|
l, err := luhn.Base32.Generate(chunk[0:13])
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
// Validate the digits match.
|
|
if fmt.Sprintf("%c", l) != chunk[13:] {
|
|
return "", errors.New("check digit incorrect")
|
|
}
|
|
|
|
res = append(res, chunk[0:13])
|
|
}
|
|
|
|
return res[0] + res[1] + res[2] + res[3], nil
|
|
}
|
|
|
|
// Returns a string split into chunks of size 7.
|
|
func chunkify(s string) string {
|
|
s = regexp.MustCompile("(.{7})").ReplaceAllString(s, "$1-")
|
|
s = strings.Trim(s, "-")
|
|
return s
|
|
}
|
|
|
|
// Un-chunks a string by removing all hyphens and spaces.
|
|
func unchunkify(s string) string {
|
|
s = strings.Replace(s, "-", "", -1)
|
|
s = strings.Replace(s, " ", "", -1)
|
|
return s
|
|
}
|
|
|
|
// We use base32 encoding, which uses 26 characters, and then the numbers
|
|
// 234567. This is useful since the alphabet doesn't contain the numbers 0, 1,
|
|
// or 8, which means we can replace them with their letter-lookalikes.
|
|
func untypeoify(s string) string {
|
|
s = strings.Replace(s, "0", "O", -1)
|
|
s = strings.Replace(s, "1", "I", -1)
|
|
s = strings.Replace(s, "8", "B", -1)
|
|
return s
|
|
}
|