diff --git a/cmd/garden/main.go b/cmd/garden/main.go new file mode 100644 index 0000000..66b437b --- /dev/null +++ b/cmd/garden/main.go @@ -0,0 +1,135 @@ +package main + +import ( + "crypto/ed25519" + "encoding/base64" + "encoding/hex" + "flag" + "fmt" + "os" + "os/signal" + "regexp" + "runtime" + "strings" + "syscall" + "time" + + "github.com/craumix/onionmsg/pkg/generator" + "github.com/craumix/onionmsg/pkg/openssh" + "github.com/craumix/onionmsg/internal/types" +) + +const ( + ups = 2 + warning = "The Content of this file is VERY sensitive!\nAll the keys here are UNENCRYPTED!\nIf you are using any of these keys, don't share them with ANYONE!\n" +) + +var ( + match = "" + count = 10 + threads = runtime.NumCPU() + anywhere = false + format = "base64" + nowarn = false + file = "" + + err error +) + +func main() { + flag.StringVar(&match, "m", match, "Specify a filter to match") + flag.BoolVar(&anywhere, "a", anywhere, "Matches anywhere (not just at the start)") + flag.IntVar(&count, "c", count, "Specify an amount of Identities to generate") + flag.IntVar(&threads, "t", threads, "Number of threads") + flag.StringVar(&format, "form", format, "The output format for the keys ( base64 / openssh / hex )") + flag.BoolVar(&nowarn, "nw", nowarn, "No warning above output") + flag.StringVar(&file, "f", file, "Output file") + flag.Parse() + + //Save cursor position & hide it + fmt.Print("\033[s\033[?25l") + //Show cursor position on exit + defer fmt.Print("\033[?25h") + func(){ + c := make(chan os.Signal) + signal.Notify(c, os.Interrupt, syscall.SIGTERM) + go func() { + <-c + fmt.Print("\033[?25h") + os.Exit(0) + }() + }() + + if !anywhere && !strings.HasPrefix(match, "^") { + match = "^" + match + } + + keys := generator.GenerateIdentities(generator.GeneratorOptions{ + Threads: threads, + Count: count, + Regex: regexp.MustCompile(match), + TickTimeout: time.Second / ups, + DidTick: func(t time.Time, pk ed25519.PrivateKey, i int, j uint64) { + lastFP := "" + if pk != nil { + lastFP = types.FingerprintKeyFormatting(pk.Public().(ed25519.PublicKey)) + } + + var keysPerSecondPerThread time.Duration = 0 + if j > 0 { + keysPerThread := float64(j) / float64(threads) + keysPerSecondPerThread = time.Duration(keysPerThread / float64(time.Since(t)/time.Second)) + } + if keysPerSecondPerThread < 0 { + keysPerSecondPerThread = 0 + } + + //Load cursor position + fmt.Print("\033[u") + fmt.Printf("Progress: [%d/%d] %d\033[K\n", i, count, j) + fmt.Printf("Time elapsed: %s\033[K\n", time.Since(t)) + fmt.Printf("Speed: avg. %d keys / second / thread\033[K\n", keysPerSecondPerThread) + fmt.Printf("Last: %s\033[K\n", lastFP) + + }, + }) + + fmt.Println() + + keyout := os.Stdout + if file != "" { + keyout, err = os.OpenFile(file, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0600) + if err != nil { + fmt.Println(err) + keyout = os.Stdout + } + + if keyout != os.Stdout { + defer keyout.Close() + } + } + + outputKeys(keys, keyout, format, !nowarn) +} + +func outputKeys(keys []ed25519.PrivateKey, out *os.File, format string, warn bool) { + if warn { + out.WriteString(warning + "\n") + } + + for _, k := range keys { + out.WriteString("Fingerprint: " + types.FingerprintKeyFormatting(k.Public().(ed25519.PublicKey)) + "\n") + out.WriteString("Key:\n" + formatKey(k, format) + "\n\n") + } +} + +func formatKey(key ed25519.PrivateKey, format string) string { + switch(format) { + case "openssh": + return string(openssh.EncodeToPemBytes(key)) + case "hex": + return hex.EncodeToString(key) + default: + return base64.StdEncoding.EncodeToString(key) + } +} \ No newline at end of file diff --git a/internal/daemon/room_util.go b/internal/daemon/room_util.go index 4a1130d..3dc8c7c 100644 --- a/internal/daemon/room_util.go +++ b/internal/daemon/room_util.go @@ -15,8 +15,6 @@ func initRooms() (err error) { } } - log.Infof("Loaded %d Rooms\n", len(data.Rooms)) - for _, room := range data.Rooms { room.RunMessageQueueForAllPeers() } diff --git a/internal/types/identity.go b/internal/types/identity.go index 559762c..4671247 100644 --- a/internal/types/identity.go +++ b/internal/types/identity.go @@ -4,6 +4,7 @@ import ( "crypto/ed25519" "encoding/base64" "fmt" + "github.com/wybiral/torgo" ) @@ -105,7 +106,15 @@ func (i Identity) Fingerprint() string { return "" } - return base64.RawURLEncoding.EncodeToString(*i.Pub) + return FingerprintKeyFormatting(*i.Pub) +} + +func FingerprintKeyFormatting(pub ed25519.PublicKey) string { + if pub == nil { + return "" + } + + return base64.RawURLEncoding.EncodeToString(pub) } func (i Identity) String() string { diff --git a/pkg/generator/generator.go b/pkg/generator/generator.go new file mode 100644 index 0000000..a6fb260 --- /dev/null +++ b/pkg/generator/generator.go @@ -0,0 +1,77 @@ +package generator + +import ( + "context" + "crypto/ed25519" + "regexp" + "time" +) + +type GeneratorOptions struct { + Threads int + Count int + Regex *regexp.Regexp + TickTimeout time.Duration + DidTick func(time.Time, ed25519.PrivateKey, int, uint64) + Ctx context.Context +} + +func DefaultGeneratorOpts() GeneratorOptions { + return GeneratorOptions{ + Threads: 1, + Count: 1, + Regex: ®exp.Regexp{}, + TickTimeout: time.Hour * 24 * 365, + Ctx: context.Background(), + } +} + +func GenerateIdentities(opts GeneratorOptions) []ed25519.PrivateKey { + if opts.Ctx == nil { + opts.Ctx = context.Background() + } + + nextKeyChan := make(chan ed25519.PrivateKey) + keys := make([]ed25519.PrivateKey, 0) + ctx, cancel := context.WithCancel(opts.Ctx) + threads := setupThreads(opts.Threads, ctx, nextKeyChan, opts.Regex) + + startTime := time.Now() + var lastKey ed25519.PrivateKey + for ctx.Err() == nil { + var count uint64 + for _, thread := range threads { + if lastKey != nil && (len(keys) == 0 || !lastKey.Equal(keys[len(keys)-1])) { + keys = append(keys, lastKey) + } + count += thread.Counter + + if opts.DidTick != nil { + opts.DidTick(startTime, lastKey, len(keys), count) + } + + if len(keys) >= opts.Count { + cancel() + } + } + + select { + case lastKey = <-nextKeyChan: + case <-ctx.Done(): + case <-time.After(opts.TickTimeout): + } + } + + return keys +} + +func setupThreads(count int, threadCtx context.Context, nextKeyChan chan ed25519.PrivateKey, regex *regexp.Regexp) []GeneratorThread { + threads := make([]GeneratorThread, count) + + for i := 0; i < count; i++ { + threads[i] = CreateGeneratorThread(threadCtx, nextKeyChan, regex) + threads[i].Start() + } + + return threads +} diff --git a/pkg/generator/generator_thread.go b/pkg/generator/generator_thread.go new file mode 100644 index 0000000..e8326ba --- /dev/null +++ b/pkg/generator/generator_thread.go @@ -0,0 +1,47 @@ +package generator + +import ( + "context" + "crypto/ed25519" + "fmt" + "os" + "regexp" + + "github.com/craumix/onionmsg/internal/types" +) + +type GeneratorThread struct { + Counter uint64 + + ctx context.Context + nextKeyChan chan ed25519.PrivateKey + regex *regexp.Regexp +} + +func CreateGeneratorThread(ctx context.Context, nextKey chan ed25519.PrivateKey, regex *regexp.Regexp) GeneratorThread { + return GeneratorThread{ + ctx: ctx, + nextKeyChan: nextKey, + regex: regex, + } +} + +func (t *GeneratorThread) Start() { + go func() { + for t.ctx.Err() == nil { + + pub, priv, err := ed25519.GenerateKey(nil) + if err != nil { + fmt.Println(err) + os.Exit(1) + } + + fingerprint := types.FingerprintKeyFormatting(pub) + if t.regex.MatchString(fingerprint) { + t.nextKeyChan <- priv + } + + t.Counter++ + } + }() +} diff --git a/pkg/openssh/openssh_decode.go b/pkg/openssh/openssh_decode.go new file mode 100644 index 0000000..2523fe0 --- /dev/null +++ b/pkg/openssh/openssh_decode.go @@ -0,0 +1,120 @@ +package openssh + +import ( + "bytes" + "crypto/ed25519" + "encoding/binary" + "encoding/pem" + "fmt" + "io/ioutil" + "log" +) + +func DecodeFromFile(filename, password string) (ed25519.PrivateKey, error) { + pemBytes, err := ioutil.ReadFile(filename) + if err != nil { + return nil, err + } + + return DecodeFromPem(pemBytes, password) +} + +func DecodeFromPem(pemBytes []byte, password string) (ed25519.PrivateKey, error) { + block, _ := pem.Decode(pemBytes) + if block == nil { + return nil, fmt.Errorf("unable to get pem block") + } + + return DecodeFromBytes(block.Bytes, password) +} + +func DecodeFromBytes(raw []byte, password string) (ed25519.PrivateKey, error) { + magicNullterm := bytes.Index(raw, []byte{0x00}) + authMagic := string(raw[:magicNullterm]) + raw = raw[magicNullterm+1:] + if authMagic != "openssh-key-v1" { + return nil, fmt.Errorf("invalid auth magic %s", authMagic) + } + + raw, ciphername := readNextString(raw) + raw, kdfname := readNextString(raw) + raw, kdfopts := readNextBytes(raw) + raw, num := readNextInt(raw) + if num != 0x01 { + return nil, fmt.Errorf("invalid value for number of keys %d", num) + } + + //Skip Public-Key length + raw = raw[4:] + + //Skip Public Key + raw, _ = readNextString(raw) + raw, _ = readNextBytes(raw) + + raw, payloadSize := readNextInt(raw) + + if ciphername == "none" && kdfname == "none" { + return keyFromPayload(raw[:payloadSize]) + } else if kdfname == "bcrypt" { + /* + For reference: + https://peterlyons.com/problog/2017/12/openssh-ed25519-private-key-file-format/ + https://crypto.stackexchange.com/questions/58536/how-does-openssh-use-bcrypt-to-set-ivs + https://cvsweb.openbsd.org/cgi-bin/cvsweb/src/usr.bin/ssh/PROTOCOL.key?annotate=HEAD + https://cvsweb.openbsd.org/cgi-bin/cvsweb/src/usr.bin/ssh/sshkey.c?rev=1.64&content-type=text/x-cvsweb-markup&only_with_tag=MAIN + */ + + salt := kdfopts[4:20] + rounds := kdfopts[20:24] + log.Println(salt) + log.Println(rounds) + return nil, fmt.Errorf("encrypted keys are currently not supported") + } else { + return nil, fmt.Errorf("unable to decrypt cipher %s with kdf %s", ciphername, kdfname) + } +} + +func keyFromPayload(payload []byte) (ed25519.PrivateKey, error) { + payload, checkint1 := readNextInt(payload) + payload, checkint2 := readNextInt(payload) + if checkint1 != checkint2 { + return nil, fmt.Errorf("checkints don't match (key probably wrong)") + } + + payload, keytype := readNextString(payload) + if keytype != "ssh-ed25519" { + return nil, fmt.Errorf("key is not of type ssh-ed25519 but of %s", keytype) + } + + //Skip what is probably a duplicate of the public key + payload, _ = readNextBytes(payload) + + payload, privatekey := readNextBytes(payload) + if len(privatekey) != 64 { + return nil, fmt.Errorf("length of private key is not 64 but %d", len(privatekey)) + } + + //Comment + _, _ = readNextString(payload) + + return ed25519.PrivateKey(privatekey), nil +} + +func readNextInt(raw []byte) (newRaw []byte, length int) { + length = int(binary.BigEndian.Uint32(raw[:4])) + newRaw = raw[4:] + return +} + +func readNextBytes(raw []byte) (newRaw, value []byte) { + raw, length := readNextInt(raw) + value = raw[:length] + newRaw = raw[length:] + return +} + +func readNextString(raw []byte) (newRaw []byte, value string) { + newRaw, tmp := readNextBytes(raw) + value = string(tmp) + return +} diff --git a/pkg/openssh/openssh_encode.go b/pkg/openssh/openssh_encode.go new file mode 100644 index 0000000..f8e9359 --- /dev/null +++ b/pkg/openssh/openssh_encode.go @@ -0,0 +1,107 @@ +package openssh + +import ( + "bytes" + "crypto/ed25519" + "crypto/rand" + "encoding/binary" + "encoding/pem" + "log" +) + +func EncodeToPemBytes(key ed25519.PrivateKey) []byte { + buffer := new(bytes.Buffer) + + block := &pem.Block{ + Type: "OPENSSH PRIVATE KEY", + Bytes: EncodeToBytes(key), + } + pem.Encode(buffer, block) + + return buffer.Bytes() +} + +//TODO key format not yet properly accepted by OpenSSH. +//Struture seems to be fine, yet the specific Pub/Priv-Key bytes, +//are wrongly formatted. (Altough I'm not sure how else you would format them ??) +func EncodeToBytes(key ed25519.PrivateKey) []byte { + buffer := new(bytes.Buffer) + + //ASCII magic "openssh-key-v1" plus null byte + buffer.WriteString("openssh-key-v1") + buffer.WriteByte(0x00) + + //Cipher + buffer.Write(intToBytes(4)) + buffer.WriteString("none") + + //KDFName + buffer.Write(intToBytes(4)) + buffer.WriteString("none") + + //KDF Iters (?) + buffer.Write(intToBytes(0)) + + //Number of Public-Keys + buffer.Write(intToBytes(1)) + + //Length of first PublicKey + buffer.Write(intToBytes(4 + 11 + 4 + 32)) + + //Key type + buffer.Write(intToBytes(11)) + buffer.WriteString("ssh-ed25519") + + //Public-Key + buffer.Write(intToBytes(32)) + buffer.Write(key.Public().(ed25519.PublicKey)) + + //Payload length + buffer.Write(intToBytes(8 + 4 + 11 + 4 + 32 + 4 + 64 + 4 + 5)) + + //Check bytes (random ?) (If Checksum FIX!!!) + check := make([]byte, 4) + c, err := rand.Read(check) + if c != len(check) || err != nil { + log.Printf("Read %d bytes should be %d", c, len(check)) + log.Fatal(err) + } + buffer.Write(check) + buffer.Write(check) + + //Key type + buffer.Write(intToBytes(11)) + buffer.WriteString("ssh-ed25519") + + //Public-Key + buffer.Write(intToBytes(32)) + buffer.Write(key.Public().(ed25519.PublicKey)) + + //Private-Key + buffer.Write(intToBytes(64)) + buffer.Write(key) + + //Zero length comment + buffer.Write(intToBytes(0)) + + //Padding + buffer.Write(incPadding(5)) + + return buffer.Bytes() +} + +func incPadding(lenght int) []byte { + p := make([]byte, lenght) + + for i := 0; i < lenght; i++ { + p[i] = uint8(i + 1) + } + + return p +} + +func intToBytes(i int) []byte { + bs := make([]byte, 4) + binary.BigEndian.PutUint32(bs, uint32(i)) + return bs +}