You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

229 lines
5.4 KiB

// TODO:
// * Board UI?
package main
import (
"bytes"
"crypto/ed25519"
"encoding/hex"
"errors"
"flag"
"fmt"
"io/ioutil"
"net/http"
"os"
"os/user"
"path/filepath"
"runtime"
"sync"
"time"
)
func validKey() (ed25519.PublicKey, ed25519.PrivateKey) {
// A conforming key's final seven hex characters must be 83e followed by
// four characters that, interpreted as MMYY, express a valid month and
// year in the range 01/00 .. 12/99. Formally, the key must match this
//
// regex: /83e(0[1-9]|1[0-2])(\d\d)$/
//
// Because we have an odd-length hex string, we don't encode the '8' here
// and instead check it specifically in the hot loop... I'm open to ideas
// about how to do this better. I'd like to keep everything in the hot loop
// using the `bytes.Equal` function which is assembly on most platforms,
// but we don't have a full byte for the `8`
keyEnd := fmt.Sprintf("3e%s", time.Now().AddDate(2, 0, 0).Format("0106"))
target, err := hex.DecodeString(keyEnd)
if err != nil {
panic(err)
}
nRoutines := runtime.NumCPU() - 1
var waitGroup sync.WaitGroup
var once sync.Once
fmt.Printf(" - looking for a key that ends in 8%s using %d routines\n",
keyEnd, nRoutines)
var publicKey ed25519.PublicKey
var privateKey ed25519.PrivateKey
start := time.Now()
waitGroup.Add(nRoutines)
for i := 0; i < nRoutines; i++ {
go func() {
for publicKey == nil {
pub, priv, err := ed25519.GenerateKey(nil)
if err != nil {
panic(err)
}
// Here's where we check for the `8`; we do it after the
// bytes.Equal to keep the hot loop fast
if bytes.Equal(pub[29:32], target) && pub[28]&0x0F == 0x08 {
once.Do(func() {
fmt.Printf("found %x in %f minutes\n", pub, time.Since(start).Minutes())
publicKey = pub
privateKey = priv
})
}
}
waitGroup.Done()
}()
}
waitGroup.Wait()
return publicKey, privateKey
}
func fileExists(name string) bool {
if _, err := os.Stat(name); errors.Is(err, os.ErrNotExist) {
return false
}
return true
}
func getKeys(folder string) (ed25519.PublicKey, ed25519.PrivateKey) {
// get the expected public key file and private key file paths
var pubfile, privfile string
if len(folder) == 0 {
user, err := user.Current()
if err != nil {
panic(err)
}
configPath := os.Getenv("XDG_CONFIG_HOME")
if configPath == "" {
configPath = filepath.Join(user.HomeDir, ".config", "spring83")
}
if err = os.MkdirAll(configPath, os.ModePerm); err != nil {
panic(err)
}
pubfile = filepath.Join(configPath, "key.pub")
privfile = filepath.Join(configPath, "key.priv")
} else {
pubfile = filepath.Join(folder, "key.pub")
privfile = filepath.Join(folder, "key.priv")
}
// try to load the public and private key files as ed25519 keys
var pubkey ed25519.PublicKey
var privkey ed25519.PrivateKey
var err error
if fileExists(pubfile) && fileExists(privfile) {
pubkey, err = ioutil.ReadFile(pubfile)
if err != nil {
panic(err)
}
privkey, err = ioutil.ReadFile(privfile)
if err != nil {
panic(err)
}
} else {
fmt.Println("Generating valid key. This will take a minute")
pubkey, privkey = validKey()
os.WriteFile(pubfile, pubkey, 0666)
os.WriteFile(privfile, privkey, 0600)
}
return pubkey, privkey
}
func getBody(inputFile string) []byte {
var body []byte
var err error
if inputFile == "-" {
body, err = ioutil.ReadAll(os.Stdin)
if err != nil {
panic(err)
}
} else {
body, err = ioutil.ReadFile(inputFile)
if err != nil {
panic(err)
}
}
// Prepend a time element. Maybe we should check to see if it's already
// been provided?
timeElt := []byte(fmt.Sprintf("<time datetime=\"%s\">", time.Now().UTC().Format(time.RFC3339)))
body = append(timeElt, body...)
if len(body) == 0 {
panic(fmt.Errorf("input required"))
}
if len(body) > 2217 {
panic(fmt.Errorf("input body too long"))
}
return body
}
func sendBody(server string, body []byte, pubkey ed25519.PublicKey, privkey ed25519.PrivateKey) {
client := &http.Client{}
url := fmt.Sprintf("%s/%x", server, pubkey)
req, err := http.NewRequest(http.MethodPut, url, bytes.NewBuffer(body))
if err != nil {
panic(err)
}
sig := ed25519.Sign(privkey, body)
req.Header.Set("Authorization", fmt.Sprintf("Spring-83 Signature=%x", sig))
dt := time.Now().Format(time.RFC1123)
req.Header.Set("If-Unmodified-Since", dt)
resp, err := client.Do(req)
if err != nil {
panic(err)
}
defer resp.Body.Close()
responseBody, err := ioutil.ReadAll(resp.Body)
if err != nil {
panic(err)
}
if resp.StatusCode != 200 {
fmt.Printf("%s: %s\n", resp.Status, responseBody)
}
}
func usage() {
fmt.Print(`springer [-file=filename] [-key=keyfolder] server
Send a spring83 board to a server
flags:
-file=filename
if present, a file to send to the server instead of accepting bytes on
stdin
-key=keyfolder
a folder the program should use for finding your public and private
keys. It will expect there to be two files, one called "key.pub" and
another called "key.priv" which are your public and private keys,
respectively
`)
os.Exit(1)
}
func main() {
inputFile := flag.String("file", "-", "The file to send to the server")
keyFolder := flag.String("key", "", "A folder to check for key.pub and key.priv")
flag.Usage = usage
flag.Parse()
if len(flag.Args()) < 1 {
usage()
}
server := flag.Args()[0]
pubkey, privkey := getKeys(*keyFolder)
sendBody(server, getBody(*inputFile), pubkey, privkey)
}