route/vendor/github.com/mmatczuk/go-http-tunnel/id/id.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
}