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.

230 lines
5.4 KiB

// TODO:
// * Board UI?
2 years ago
package main
import (
"bytes"
"crypto/ed25519"
"encoding/hex"
"errors"
"flag"
2 years ago
"fmt"
"io/ioutil"
"net/http"
"os"
"os/user"
"path/filepath"
"runtime"
"sync"
2 years ago
"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)
}
2 years ago
nRoutines := runtime.NumCPU() - 1
var waitGroup sync.WaitGroup
var once sync.Once
2 years ago
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()
}()
2 years ago
}
waitGroup.Wait()
return publicKey, privateKey
2 years ago
}
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)
}
2 years ago
configPath := os.Getenv("XDG_CONFIG_HOME")
if configPath == "" {
configPath = filepath.Join(user.HomeDir, ".config", "spring83")
}
2 years ago
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")
2 years ago
}
// try to load the public and private key files as ed25519 keys
2 years ago
var pubkey ed25519.PublicKey
var privkey ed25519.PrivateKey
var err error
2 years ago
if fileExists(pubfile) && fileExists(privfile) {
pubkey, err = ioutil.ReadFile(pubfile)
if err != nil {
panic(err)
}
2 years ago
privkey, err = ioutil.ReadFile(privfile)
if err != nil {
panic(err)
}
} else {
fmt.Println("Generating valid key. This will take a minute")
2 years ago
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)
}
2 years ago
}
// 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...)
2 years ago
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)
2 years ago
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)
2 years ago
}