├── .gitignore ├── LICENSE ├── README ├── chain.go ├── chunker.go ├── client.go ├── config ├── config.go └── config_test.go ├── crandom ├── crandom.go └── crandom_test.go ├── crypto.go ├── doc └── yamn.1 ├── examples ├── yamn.cfg.client └── yamn.cfg.remailer ├── go.mod ├── go.sum ├── idlog └── idlog.go ├── keymgr ├── public.go ├── public_test.go └── secret.go ├── linebreaker ├── linebreaker.go └── linebreaker_test.go ├── mail.go ├── packet.go ├── packet_test.go ├── pool.go ├── quickmail └── quickmail.go ├── server.go ├── stats.go ├── utils.go ├── www └── yamn_packet.html └── yamn.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | *.test 24 | *.prof 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Steve Crook 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | Yet Another Mix Net - A Mixmaster derivative 2 | 3 | YAMN tries to look like Mixmaster in order to maintain compatibility with the 4 | various GUI's and peripheral applications that serve to make it a complete 5 | solution. Under the covers there will be some significant differences:- 6 | 7 | * Revised crypto functions. Instead of Mixmaster's Triple-DES, RSAES-PKCS1-v1_5 8 | and MD5, YAMN will use AES, NaCl Box and Blake2. 9 | 10 | * Deterministic header padding to protect against tagging attacks 11 | 12 | * Others TBA 13 | 14 | Installation from source:- 15 | go get github.com/crooks/yamn 16 | go get code.google.com/p/go.crypto/nacl/box 17 | go get github.com/syndtr/goleveldb 18 | go get github.com/luksen/maildir 19 | cd ~/go/src/github.com/crooks/yamn 20 | go build 21 | 22 | Post-install configuration:- 23 | Create a dir for yamn. (mkdir yamn) 24 | Copy yamn binary (yamn.exe on Windows) to the above dir 25 | Copy yamn.cfg.sample to same dir and rename to yamn.cfg 26 | Modify yamn.cfg to meet your requirements 27 | 28 | Examples:- 29 | 30 | Send an anonymous email 31 | yamn --mail msg.txt 32 | 33 | Use a user-defined chain 34 | yamn --mail --chain="*,*,yamn" msg.txt 35 | 36 | Send multiple copies 37 | yamn --mail --copies=2 msg.txt 38 | 39 | Perform remailer functions 40 | yamn --remailer 41 | 42 | Start a remailer daemon 43 | yamn --remailer --daemon 44 | -------------------------------------------------------------------------------- /chain.go: -------------------------------------------------------------------------------- 1 | // vim: tabstop=2 shiftwidth=2 2 | 3 | package main 4 | 5 | import ( 6 | "errors" 7 | "fmt" 8 | "os" 9 | 10 | "github.com/Masterminds/log-go" 11 | "github.com/crooks/yamn/crandom" 12 | "github.com/crooks/yamn/keymgr" 13 | ) 14 | 15 | // distanceCriteria enforces user-defined minimal distance criteria 16 | func distanceCriteria(addresses, dist []string) (c []string) { 17 | for _, addy := range addresses { 18 | if IsMemberStr(addy, dist) { 19 | // Excluded due to distance 20 | continue 21 | } 22 | c = append(c, addy) 23 | } 24 | return 25 | } 26 | 27 | // makeChain takes a chain string and constructs a valid remailer chain 28 | func makeChain(inChain []string) (outChain []string, err error) { 29 | // Test if stats file has been modified since last imported 30 | if Pubring.StatRefresh() { 31 | // Try and import the modified stats file 32 | err = Pubring.ImportStats() 33 | if err != nil { 34 | log.Warnf("Unable to read stats: %s", err) 35 | return 36 | } 37 | log.Info("Stats updated and reimported") 38 | } 39 | // Check generated timestamp from stats file 40 | if Pubring.HaveStats() && Pubring.StatsStale(cfg.Stats.StaleHrs) { 41 | log.Warnf( 42 | "Stale stats. Generated age exceeds "+ 43 | "configured threshold of %d hours", 44 | cfg.Stats.StaleHrs, 45 | ) 46 | } 47 | // If the chain contains a random remailer, we're going to need stats 48 | if !Pubring.HaveStats() && IsMemberStr("*", inChain) { 49 | err = errors.New("cannot use random remailers without stats") 50 | log.Warn(err) 51 | return 52 | } 53 | dist := cfg.Stats.Distance 54 | if dist > maxChainLength { 55 | dist = maxChainLength 56 | } 57 | var candidates []string // Candidate remailers for each hop 58 | if len(inChain) > maxChainLength { 59 | fmt.Fprintf(os.Stderr, "%d hops exceeds maximum of %d\n", len(inChain), maxChainLength) 60 | os.Exit(1) 61 | } 62 | // If dist is greater than the actual chain length, all hops will be unique. 63 | if dist > len(inChain) { 64 | dist = len(inChain) 65 | } 66 | var distance []string 67 | inDist := make([]string, 0, dist) // n distance elements of input chain 68 | outDist := make([]string, 0, dist) // n distance elements of output chain 69 | outChain = make([]string, 0, len(inChain)) 70 | // Loop until inChain contains no more remailers 71 | numHops := len(inChain) 72 | var hop string 73 | for { 74 | hop = popstr(&inChain) 75 | if hop == "*" { 76 | // Random remailer selection 77 | if len(outChain) == 0 { 78 | // Construct a list of suitable exit remailers 79 | candidates = Pubring.Candidates( 80 | cfg.Stats.Minlat, 81 | cfg.Stats.Maxlat, 82 | cfg.Stats.Relfinal, 83 | true) 84 | } else { 85 | // Construct a list of all suitable remailers 86 | candidates = Pubring.Candidates( 87 | cfg.Stats.Minlat, 88 | cfg.Stats.Maxlat, 89 | cfg.Stats.Minrel, 90 | false) 91 | } 92 | if len(candidates) > 0 { 93 | // Apply distance criteria 94 | candidates = distanceCriteria(candidates, distance) 95 | if len(candidates) == 0 { 96 | log.Warn("Insufficient remailers to comply with distance criteria") 97 | } 98 | } else { 99 | log.Warn("No candidate remailers match selection criteria") 100 | } 101 | 102 | if len(candidates) == 0 && flag.Remailer { 103 | log.Warn("Relaxing latency and uptime criteria to build chain") 104 | if len(outChain) == 0 { 105 | // Construct a list of suitable exit remailers 106 | log.Info("Constructing relaxed list of Exit remailers") 107 | candidates = Pubring.Candidates(0, 480, 0, true) 108 | log.Infof( 109 | "Discovered %d Exit Remailers matching relaxed criteria", 110 | len(candidates), 111 | ) 112 | } else { 113 | // Construct a list of all suitable remailers 114 | log.Info("Constructing relaxed list of candidate remailers") 115 | candidates = Pubring.Candidates(0, 480, 0, false) 116 | log.Infof( 117 | "Discovered %d candidate Remailers matching relaxed criteria", 118 | len(candidates), 119 | ) 120 | } 121 | } else if len(candidates) == 0 { 122 | // Insufficient remailers meet criteria and we're a client, so die. 123 | os.Exit(1) 124 | } 125 | if len(candidates) == 0 { 126 | err = errors.New("no remailers available to build random chain link") 127 | return 128 | } else if len(candidates) == 1 { 129 | hop = candidates[0] 130 | log.Warnf( 131 | "Only one remailer (%s) meets chain "+ 132 | "criteria", 133 | hop, 134 | ) 135 | } else { 136 | hop = candidates[crandom.RandomInt(len(candidates))] 137 | } 138 | } else { 139 | var remailer keymgr.Remailer 140 | remailer, err = Pubring.Get(hop) 141 | if err != nil { 142 | return 143 | } 144 | hop = remailer.Address 145 | } 146 | // Extend outChain by 1 element 147 | outChain = outChain[0 : len(outChain)+1] 148 | // Shuffle existing entries to the right 149 | copy(outChain[1:], outChain[:len(outChain)-1]) 150 | // Insert new hop at the start of the output chain 151 | outChain[0] = hop 152 | 153 | // Break out when the input chain is empty 154 | if len(inChain) == 0 { 155 | break 156 | } 157 | // The following section is concerned with distance parameter compliance 158 | if len(outChain) > dist { 159 | outDist = outChain[:dist] 160 | } else { 161 | outDist = outChain 162 | } 163 | if len(inChain) > dist { 164 | inDist = inChain[len(inChain)-dist:] 165 | } else { 166 | inDist = inChain 167 | } 168 | distance = append(inDist, outDist...) 169 | } 170 | if len(outChain) != numHops { 171 | panic("Constructed chain length doesn't match input chain length") 172 | } 173 | return 174 | } 175 | -------------------------------------------------------------------------------- /chunker.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "os" 7 | "path" 8 | "strings" 9 | "time" 10 | 11 | "github.com/Masterminds/log-go" 12 | "github.com/syndtr/goleveldb/leveldb" 13 | ) 14 | 15 | // Chunk holds variables that apply globally to all chunked message content. 16 | type Chunk struct { 17 | db *leveldb.DB // A level DB instance 18 | expireDays time.Duration // How long to retain keys 19 | deleteDays time.Duration // Age of partial files before deletion 20 | } 21 | 22 | // OpenChunk opens a levelDB database file 23 | func OpenChunk(filename string) *Chunk { 24 | levelDB, err := leveldb.OpenFile(filename, nil) 25 | if err != nil { 26 | panic(err) 27 | } 28 | return &Chunk{db: levelDB} 29 | } 30 | 31 | // Close closes the levelDB 32 | func (chunk *Chunk) Close() { 33 | chunk.db.Close() 34 | } 35 | 36 | // SetExpire defines how long keys should be retained in the DB 37 | func (chunk *Chunk) SetExpire(days int) { 38 | chunk.expireDays = time.Duration(days*24) * time.Hour 39 | chunk.deleteDays = time.Duration(days*24*2) * time.Hour 40 | } 41 | 42 | // Get returns the content associated with messageID. If messageID is 43 | // unknown, an empty slice of len numChunks is returned. 44 | func (chunk *Chunk) Get(messageID []byte, numChunks int) (items []string) { 45 | var err error 46 | var content []byte 47 | content, err = chunk.db.Get(messageID, nil) 48 | if err != nil { 49 | if err.Error() == "leveldb: not found" { 50 | items = make([]string, numChunks) 51 | } else { 52 | panic(err) 53 | } 54 | } else { 55 | // Ignore the first item in the DB, it's the date field and 56 | // only used internally. 57 | items = strings.Split(string(content), ",")[1:] 58 | } 59 | return 60 | } 61 | 62 | // Insert writes an entry to the DB 63 | func (chunk *Chunk) Insert(messageid []byte, items []string) { 64 | if chunk.expireDays == 0 { 65 | panic("Expiry duration not defined") 66 | } 67 | t := time.Now().Add(chunk.expireDays) 68 | chunk.db.Put( 69 | messageid, 70 | []byte(fmt.Sprintf( 71 | "%s,%s", 72 | t.Format("20060102"), 73 | strings.Join(items, ","), 74 | )), 75 | nil, 76 | ) 77 | } 78 | 79 | // IsPopulated returns true if all elements of items are populated 80 | func IsPopulated(items []string) bool { 81 | for _, n := range items { 82 | if n == "" { 83 | return false 84 | } 85 | } 86 | return true 87 | } 88 | 89 | // Housekeep deletes files over a given age 90 | func (chunk *Chunk) Housekeep() (ret, del int) { 91 | files, err := ioutil.ReadDir(cfg.Files.Pooldir) 92 | if err != nil { 93 | log.Warnf("Chunk housekeeping failed: %s", err) 94 | return 95 | } 96 | expire := time.Now().Add(-chunk.deleteDays) 97 | for _, file := range files { 98 | if !strings.HasPrefix(file.Name(), "p") { 99 | // Don't do anything unless the file begins with "p" 100 | continue 101 | } 102 | if file.ModTime().Before(expire) { 103 | os.Remove(path.Join(cfg.Files.Pooldir, file.Name())) 104 | del++ 105 | } else { 106 | ret++ 107 | } 108 | } 109 | return 110 | } 111 | 112 | // Assemble takes all the file chunks, assembles them in order and stores the 113 | // result back into the pool. 114 | func (chunk *Chunk) Assemble(filename string, items []string) (err error) { 115 | f, err := os.Create(filename) 116 | if err != nil { 117 | return 118 | } 119 | defer f.Close() 120 | var content []byte 121 | for _, c := range items { 122 | infile := path.Join(cfg.Files.Pooldir, c) 123 | content, err = ioutil.ReadFile(infile) 124 | if err != nil { 125 | log.Warnf("Chunk assembler says: %s", err) 126 | continue 127 | } 128 | f.Write(content) 129 | err = os.Remove(infile) 130 | if err != nil { 131 | log.Warnf("Assembler chunk delete failed: %s", err) 132 | continue 133 | } 134 | } 135 | return 136 | } 137 | 138 | // Delete removes the specified Message ID from the DB 139 | func (chunk *Chunk) Delete(messageid []byte) { 140 | err := chunk.db.Delete(messageid, nil) 141 | if err != nil { 142 | log.Warnf("Could not delete MsgID: %s. %s", messageid, err) 143 | } 144 | } 145 | 146 | // DeleteItems removes all the filename defined in items 147 | func (chunk *Chunk) DeleteItems(items []string) (deleted, failed int) { 148 | for _, file := range items { 149 | fqfn := path.Join(cfg.Files.Pooldir, file) 150 | err := os.Remove(fqfn) 151 | if err != nil { 152 | failed++ 153 | } else { 154 | deleted++ 155 | } 156 | } 157 | return 158 | } 159 | 160 | // Expire iterates the DB and deletes entries (and files) that exceed the 161 | // defined age. 162 | func (chunk *Chunk) Expire() (retained, deleted int) { 163 | now := time.Now() 164 | iter := chunk.db.NewIterator(nil, nil) 165 | for iter.Next() { 166 | key := iter.Key() 167 | items := strings.Split(string(iter.Value()), ",") 168 | stamp := items[0] 169 | // Now we have the timestamp, strip it from the slice 170 | copy(items, items[1:]) 171 | expire, err := time.Parse("20060102", stamp) 172 | if err != nil { 173 | log.Warnf("Could not parse timestamp: %s", err) 174 | // If the timestamp is invalid, delete the record 175 | chunk.DeleteItems(items) 176 | chunk.Delete(key) 177 | } 178 | if expire.Before(now) { 179 | chunk.DeleteItems(items) 180 | chunk.Delete(key) 181 | deleted++ 182 | } else { 183 | retained++ 184 | } 185 | } 186 | return 187 | } 188 | -------------------------------------------------------------------------------- /client.go: -------------------------------------------------------------------------------- 1 | // vim: tabstop=2 shiftwidth=2 2 | 3 | package main 4 | 5 | import ( 6 | "errors" 7 | "fmt" 8 | "io/ioutil" 9 | "math" 10 | "net/mail" 11 | "os" 12 | "strings" 13 | "time" 14 | 15 | "github.com/Masterminds/log-go" 16 | "github.com/crooks/yamn/crandom" 17 | "github.com/crooks/yamn/keymgr" 18 | //"github.com/codahale/blake2" 19 | ) 20 | 21 | // readMessage tries to read a file containing the plaintext to be sent 22 | func readMessage(filename string) []byte { 23 | var err error 24 | f, err := os.Open(filename) 25 | if err != nil { 26 | fmt.Fprintf(os.Stderr, "%s: Unable to open file\n", filename) 27 | os.Exit(1) 28 | } 29 | msg, err := mail.ReadMessage(f) 30 | if err != nil { 31 | fmt.Fprintf(os.Stderr, "%s: Malformed mail message\n", filename) 32 | os.Exit(1) 33 | } 34 | if flag.To != "" { 35 | msg.Header["To"] = []string{flag.To} 36 | if !strings.Contains(flag.To, "@") { 37 | fmt.Fprintf( 38 | os.Stderr, 39 | "%s: Recipient doesn't appear to be an "+ 40 | "email address\n", 41 | flag.To, 42 | ) 43 | } 44 | } 45 | if flag.Subject != "" { 46 | msg.Header["Subject"] = []string{flag.Subject} 47 | } 48 | return assemble(*msg) 49 | } 50 | 51 | // mixprep fetches the plaintext and prepares it for mix encoding 52 | func mixprep() { 53 | var err error 54 | err = os.MkdirAll(cfg.Files.Pooldir, 0700) 55 | if err != nil { 56 | panic(err) 57 | } 58 | // plain will contain the byte version of the plain text message 59 | var plain []byte 60 | // final is consistent across multiple copies so we define it early 61 | final := newSlotFinal() 62 | if len(flag.Args) == 0 { 63 | //fmt.Println("Enter message, complete with headers. Ctrl-D to finish") 64 | plain, err = ioutil.ReadAll(os.Stdin) 65 | if err != nil { 66 | fmt.Fprintln(os.Stderr, err) 67 | os.Exit(1) 68 | } 69 | } else if len(flag.Args) == 1 { 70 | // A single arg should be the filename 71 | plain = readMessage(flag.Args[0]) 72 | } else if len(flag.Args) >= 2 { 73 | // Two args should be recipient and filename 74 | flag.To = flag.Args[0] 75 | plain = readMessage(flag.Args[1]) 76 | } 77 | // plainLen is the length of the plain byte message and can exceed 78 | // the total body size of the payload. 79 | plainLen := len(plain) 80 | if plainLen == 0 { 81 | fmt.Fprintln(os.Stderr, "No bytes in message") 82 | os.Exit(1) 83 | } 84 | 85 | // Download stats URLs if the time is right 86 | if cfg.Urls.Fetch { 87 | // Retrieve Mlist2 and Pubring URLs 88 | timedURLFetch(cfg.Urls.Pubring, cfg.Files.Pubring) 89 | timedURLFetch(cfg.Urls.Mlist2, cfg.Files.Mlist2) 90 | } 91 | 92 | // Create the Public Keyring 93 | Pubring = keymgr.NewPubring( 94 | cfg.Files.Pubring, 95 | cfg.Files.Mlist2, 96 | ) 97 | // Set the Use Expired flag to include remailers with expired keys as 98 | // candidates. 99 | if cfg.Stats.UseExpired { 100 | Pubring.UseExpired() 101 | } 102 | err = Pubring.ImportPubring() 103 | if err != nil { 104 | log.Warnf("Pubring import failed: %s", cfg.Files.Pubring) 105 | return 106 | } 107 | // Read the chain from flag or config 108 | var inChain []string 109 | var inChainFunc []string 110 | if flag.Chain == "" { 111 | inChain = strings.Split(cfg.Stats.Chain, ",") 112 | } else { 113 | inChain = strings.Split(flag.Chain, ",") 114 | } 115 | if len(inChain) == 0 { 116 | err = errors.New("empty input chain") 117 | return 118 | } 119 | var cnum int // Chunk number 120 | var numc int // Number of chunks 121 | numc = int(math.Ceil(float64(plainLen) / float64(maxFragLength))) 122 | final.setNumChunks(numc) 123 | var exitnode string // Address of exit node (for multiple copy chains) 124 | var gotExit bool // Flag to indicate an exit node has been selected 125 | var firstByte int // First byte of message slice 126 | var lastByte int // Last byte of message slice 127 | // Fragments loop begins here 128 | for cnum = 1; cnum <= numc; cnum++ { 129 | final.setChunkNum(cnum) 130 | // First byte of message fragment 131 | firstByte = (cnum - 1) * maxFragLength 132 | lastByte = firstByte + maxFragLength 133 | // Don't slice beyond the end of the message 134 | if lastByte > plainLen { 135 | lastByte = plainLen 136 | } 137 | gotExit = false 138 | // If no copies flag is specified, use the config file NUMCOPIES 139 | if flag.Copies == 0 { 140 | flag.Copies = cfg.Stats.Numcopies 141 | } 142 | if flag.Copies > maxCopies { 143 | // Limit copies to a maximum of 10 144 | flag.Copies = maxCopies 145 | } 146 | // Copies loop begins here 147 | for n := 0; n < flag.Copies; n++ { 148 | if gotExit { 149 | // Set the last node in the chain to the 150 | // previously select exitnode 151 | inChain[len(inChain)-1] = exitnode 152 | } 153 | var chain []string 154 | inChainFunc = append(inChain[:0:0], inChain...) 155 | chain, err = makeChain(inChainFunc) 156 | if err != nil { 157 | log.Error(err) 158 | os.Exit(0) 159 | } 160 | if len(chain) != len(inChain) { 161 | err = fmt.Errorf("chain length mismatch: in=%d, out=%d", len(inChain), len(chain)) 162 | panic(err) 163 | } 164 | //fmt.Println(chain) 165 | if !gotExit { 166 | exitnode = chain[len(chain)-1] 167 | gotExit = true 168 | } 169 | // Retain the entry hop. We need to mail the message to it. 170 | sendTo := chain[0] 171 | // Report the chain if we're running as a client. 172 | if flag.Client { 173 | log.Infof("Chain: %s\n", strings.Join(chain, ",")) 174 | } 175 | yamnMsg := encodeMsg( 176 | plain[firstByte:lastByte], 177 | chain, 178 | *final, 179 | ) 180 | writeMessageToPool(sendTo, yamnMsg) 181 | } // End of copies loop 182 | } // End of fragments loop 183 | 184 | // Decide if we want to inject a dummy 185 | if !flag.NoDummy && Pubring.HaveStats() && crandom.Dice() < 80 { 186 | dummy() 187 | } 188 | } 189 | 190 | // encodeMsg encodes a plaintext fragment into mixmaster format. 191 | func encodeMsg( 192 | plain []byte, 193 | chain []string, 194 | final slotFinal) []byte { 195 | 196 | var err error 197 | var hop string 198 | m := newEncMessage() 199 | m.setChainLength(len(chain)) 200 | length := m.setPlainText(plain) 201 | // Pop the exit remailer address from the chain 202 | hop = popstr(&chain) 203 | // Insert the plain message length into the Final Hop header. 204 | 205 | final.setBodyBytes(length) 206 | slotData := newSlotData() 207 | // Identify this hop as Packet-Type 1 (Exit). 208 | slotData.setExit() 209 | // For exit hops, the AES key can be entirely random. 210 | slotData.setAesKey(crandom.Randbytes(32)) 211 | // Override the random PacketID so that multi-copy messages all share a 212 | // common Exit PacketID. 213 | slotData.setPacketID(final.getPacketID()) 214 | // Encode the (final) Packet Info and store it in the Slot Data. 215 | slotData.setPacketInfo(final.encode()) 216 | // Get KeyID and NaCl PK for the remailer we're enrypting to. 217 | remailer, err := Pubring.Get(hop) 218 | if err != nil { 219 | log.Errorf( 220 | "%s: Remailer unknown in public keyring\n", 221 | hop, 222 | ) 223 | os.Exit(1) 224 | } 225 | // Create a new Header. 226 | header := newEncodeHeader() 227 | // Tell the header function what KeyID and PK to NaCl encrypt with. 228 | header.setRecipient(remailer.Keyid, remailer.PK) 229 | log.Tracef( 230 | "Encrypting Final Hop: Hop=%s, KeyID=%x", 231 | hop, 232 | remailer.Keyid, 233 | ) 234 | // Only the body needs to be encrypted during Exit encoding. At all other 235 | // hops, the entire header stack will also need encrypting. 236 | m.encryptBody(slotData.aesKey, final.aesIV) 237 | // Shift all the header down by headerBytes 238 | m.shiftHeaders() 239 | // We've already popped an entry from the Chain so were testing for 240 | // length greater than zero rather than 1. 241 | if len(chain) > 0 { 242 | // Single hop chains don't require deterministic headers. All 243 | // longer chains do. 244 | m.deterministic(0) 245 | } 246 | // Set the Anti-tag hash in the slotData. 247 | slotData.setTagHash(m.getAntiTag()) 248 | // Encode the slot data into Byte form. 249 | slotDataBytes := slotData.encode() 250 | // Encode the header and insert it into the payload. 251 | m.insertHeader(header.encode(slotDataBytes)) 252 | 253 | // That concludes Exit hop compilation. Now for intermediates. 254 | 255 | interHops := m.getIntermediateHops() 256 | for interHop := 0; interHop < interHops; interHop++ { 257 | inter := newSlotIntermediate() 258 | inter.setPartialIV(m.getPartialIV(interHop)) 259 | // hop still contains the previous iteration (or exit) address. 260 | inter.setNextHop(hop) 261 | // Pop another remailer from the left side of the Chain 262 | hop = popstr(&chain) 263 | // Create new Slot Data 264 | slotData = newSlotData() 265 | slotData.setAesKey(m.getKey(interHop)) 266 | slotData.setPacketInfo(inter.encode()) 267 | m.encryptAll(interHop) 268 | m.shiftHeaders() 269 | m.deterministic(interHop + 1) 270 | slotData.setTagHash(m.getAntiTag()) 271 | slotDataBytes = slotData.encode() 272 | header = newEncodeHeader() 273 | remailer, err := Pubring.Get(hop) 274 | if err != nil { 275 | log.Errorf( 276 | "%s: Remailer unknown in public keyring\n", 277 | hop, 278 | ) 279 | os.Exit(1) 280 | } 281 | header.setRecipient(remailer.Keyid, remailer.PK) 282 | log.Tracef( 283 | "Encrypting: Hop=%s, KeyID=%x", 284 | hop, 285 | remailer.Keyid, 286 | ) 287 | m.insertHeader(header.encode(slotDataBytes)) 288 | } 289 | if len(chain) != 0 { 290 | panic("After encoding, chain was not empty.") 291 | } 292 | return m.getPayload() 293 | } 294 | 295 | func injectDummy() { 296 | // Populate public keyring 297 | Pubring = keymgr.NewPubring( 298 | cfg.Files.Pubring, 299 | cfg.Files.Mlist2, 300 | ) 301 | Pubring.ImportPubring() 302 | dummy() 303 | } 304 | 305 | // TimedURLFetch attempts to read a url into a file if the file is more 306 | // than an hour old or doesn't exist. 307 | func timedURLFetch(url, filename string) { 308 | var err error 309 | var stamp time.Time 310 | var doFetch bool 311 | if cfg.Urls.Fetch { 312 | stamp, err = fileTime(filename) 313 | if err != nil { 314 | doFetch = true 315 | } else if time.Since(stamp) > time.Hour { 316 | doFetch = true 317 | } else { 318 | doFetch = false 319 | } 320 | if doFetch { 321 | log.Infof("Fetching %s and storing in %s", url, filename) 322 | err = httpGet(url, filename) 323 | if err != nil { 324 | log.Warn(err) 325 | } 326 | } 327 | } 328 | } 329 | 330 | // dummy is a simplified client function that sends dummy messages 331 | func dummy() { 332 | var err error 333 | plainMsg := []byte("I hope Len approves") 334 | // Make a single hop chain with a random node 335 | var inChain []string 336 | if flag.Chain == "" { 337 | inChain = []string{"*", "*"} 338 | } else { 339 | inChain = strings.Split(flag.Chain, ",") 340 | } 341 | final := newSlotFinal() 342 | // Override the default delivery method (255 = Dummy) 343 | final.setDeliveryMethod(255) 344 | var chain []string 345 | chain, err = makeChain(inChain) 346 | sendTo := chain[0] 347 | if err != nil { 348 | log.Warnf("Dummy creation failed: %s", err) 349 | return 350 | } 351 | log.Tracef("Sending dummy through: %s.", strings.Join(chain, ",")) 352 | yamnMsg := encodeMsg(plainMsg, chain, *final) 353 | writeMessageToPool(sendTo, yamnMsg) 354 | return 355 | } 356 | -------------------------------------------------------------------------------- /config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "io/ioutil" 7 | "os" 8 | "path" 9 | 10 | "gopkg.in/yaml.v3" 11 | ) 12 | 13 | // Config contains all the configuration settings for Yamn. 14 | type Config struct { 15 | General struct { 16 | Loglevel string `yaml:"loglevel"` 17 | LogToFile bool `yaml:"logtofile"` 18 | LogToJournal bool `yaml:"logtojournal"` 19 | } `yaml:"general"` 20 | Files struct { 21 | // Config is a special variable that returns the name of the active config file. 22 | // If it's set in the config file, it will be ignored. 23 | Config string 24 | Pubring string `yaml:"pubring"` 25 | Mlist2 string `yaml:"mlist2"` 26 | Pubkey string `yaml:"pubkey"` 27 | Secring string `yaml:"secring"` 28 | Adminkey string `yaml:"adminkey"` 29 | Help string `yaml:"help"` 30 | Pooldir string `yaml:"pooldir"` 31 | Maildir string `yaml:"maildir"` 32 | IDlog string `yaml:"idlog"` 33 | ChunkDB string `yaml:"chunkdb"` 34 | Logfile string `yaml:"logfile"` 35 | } `yaml:"files"` 36 | Urls struct { 37 | Fetch bool `yaml:"fetch"` 38 | Pubring string `yaml:"pubring"` 39 | Mlist2 string `yaml:"mlist2"` 40 | } `yaml:"urls"` 41 | Mail struct { 42 | Sendmail bool `yaml:"sendmail"` 43 | Pipe string `yaml:"pipe"` 44 | Outfile bool `yaml:"outfile"` 45 | UseTLS bool `yaml:"usetls"` 46 | SMTPRelay string `yaml:"smtp_relay"` 47 | SMTPPort int `yaml:"smtp_port"` 48 | MXRelay bool `yaml:"mx_relay"` 49 | OnionRelay bool `yaml:"onion_relay"` 50 | Sender string `yaml:"sender"` 51 | Username string `yaml:"username"` 52 | Password string `yaml:"password"` 53 | OutboundName string `yaml:"outbound_name"` 54 | OutboundAddy string `yaml:"outbound_addy"` 55 | CustomFrom bool `yaml:"custom_from"` 56 | } `yaml:"mail"` 57 | Stats struct { 58 | Minlat int `yaml:"minlat"` 59 | Maxlat int `yaml:"maxlat"` 60 | Minrel float32 `yaml:"minrel"` 61 | Relfinal float32 `yaml:"rel_final"` 62 | Chain string `yaml:"chain"` 63 | Numcopies int `yaml:"num_copies"` 64 | Distance int `yaml:"distance"` 65 | StaleHrs int `yaml:"stale_hours"` 66 | UseExpired bool `yaml:"use_expired"` 67 | } `yaml:"stats"` 68 | Pool struct { 69 | Size int `yaml:"size"` 70 | Rate int `yaml:"rate"` 71 | MinSend int `yaml:"min_send"` 72 | Loop int `yaml:"loop"` 73 | // Delete excessively old messages from the outbound pool 74 | MaxAge int `yaml:"max_age"` 75 | } `yaml:"pool"` 76 | Remailer struct { 77 | Name string `yaml:"name"` 78 | Address string `yaml:"address"` 79 | Exit bool `yaml:"exit"` 80 | MaxSize int `yaml:"max_size"` 81 | IDexp int `yaml:"id_expire"` 82 | ChunkExpire int `yaml:"chunk_expire"` 83 | MaxAge int `yaml:"max_age"` 84 | Keylife int `yaml:"key_life"` 85 | Keygrace int `yaml:"key_grace"` 86 | Daemon bool `yaml:"daemon"` 87 | } `yaml:"remailer"` 88 | } 89 | 90 | type Flags struct { 91 | Dir string 92 | Debug bool 93 | Client bool 94 | Send bool 95 | Refresh bool 96 | Remailer bool 97 | Daemon bool 98 | Chain string 99 | To string 100 | Subject string 101 | Args []string 102 | Config string 103 | Copies int 104 | Stdin bool 105 | Stdout bool 106 | Dummy bool 107 | NoDummy bool 108 | Version bool 109 | MemInfo bool 110 | } 111 | 112 | // GetCfg parses the command line flags and config file if they haven't been previously parsed. 113 | // This should be an init function but tests fail if flags are parsed in init. 114 | // See: https://github.com/golang/go/issues/46869 115 | func GetCfg() (*Flags, *Config) { 116 | var err error 117 | f := ParseFlags() 118 | // Some config defaults are derived from flags so ParseConfig is a flags method 119 | c, err := f.ParseConfig() 120 | if err != nil { 121 | // No logging is defined at this point so log the error to stderr 122 | fmt.Fprintf(os.Stderr, "Unable to parse config file: %v", err) 123 | os.Exit(1) 124 | } 125 | return f, c 126 | } 127 | 128 | // WriteConfig will write the current config to a given filename 129 | func (c *Config) WriteConfig(filename string) error { 130 | data, err := yaml.Marshal(c) 131 | if err != nil { 132 | return err 133 | } 134 | err = ioutil.WriteFile(filename, data, 0644) 135 | if err != nil { 136 | return err 137 | } 138 | return nil 139 | } 140 | 141 | func (c *Config) Debug() ([]byte, error) { 142 | data, err := yaml.Marshal(c) 143 | if err != nil { 144 | return nil, err 145 | } 146 | return data, err 147 | } 148 | 149 | func ParseFlags() *Flags { 150 | f := new(Flags) 151 | // Base DIR for easy setting of some default file paths 152 | flag.StringVar(&f.Dir, "dir", "", "Base DIR for YAMN files") 153 | // Function as a client 154 | flag.BoolVar(&f.Client, "mail", false, "Function as a client") 155 | flag.BoolVar(&f.Client, "m", false, "Function as a client") 156 | // Send (from pool) 157 | flag.BoolVar(&f.Send, "send", false, "Force pool send") 158 | flag.BoolVar(&f.Send, "S", false, "Force pool send") 159 | // Perform remailer actions 160 | flag.BoolVar(&f.Remailer, "remailer", false, "Perform routine remailer actions") 161 | flag.BoolVar(&f.Remailer, "M", false, "Perform routine remailer actions") 162 | // Start remailer as a daemon 163 | flag.BoolVar(&f.Daemon, "daemon", false, "Start remailer as a daemon. (Requires -M)") 164 | flag.BoolVar(&f.Daemon, "D", false, "Start remailer as a daemon. (Requires -M)") 165 | // Remailer chain 166 | flag.StringVar(&f.Chain, "chain", "", "Remailer chain") 167 | flag.StringVar(&f.Chain, "l", "", "Remailer chain") 168 | // Recipient address 169 | flag.StringVar(&f.To, "to", "", "Recipient email address") 170 | flag.StringVar(&f.To, "t", "", "Recipient email address") 171 | // Subject header 172 | flag.StringVar(&f.Subject, "subject", "", "Subject header") 173 | flag.StringVar(&f.Subject, "s", "", "Subject header") 174 | // Number of copies 175 | flag.IntVar(&f.Copies, "copies", 2, "Number of copies") 176 | flag.IntVar(&f.Copies, "c", 2, "Number of copies") 177 | // Config file 178 | flag.StringVar(&f.Config, "config", "", "Config file") 179 | // Read STDIN 180 | flag.BoolVar(&f.Stdin, "read-mail", false, "Read a message from stdin") 181 | flag.BoolVar(&f.Stdin, "R", false, "Read a message from stdin") 182 | // Write to STDOUT 183 | flag.BoolVar(&f.Stdout, "stdout", false, "Write message to stdout") 184 | // Inject dummy 185 | flag.BoolVar(&f.Dummy, "dummy", false, "Inject a dummy message") 186 | flag.BoolVar(&f.Dummy, "d", false, "Inject a dummy message") 187 | // Disable dummy messaging 188 | flag.BoolVar(&f.NoDummy, "nodummy", false, "Don't send dummies") 189 | // Print Version 190 | flag.BoolVar(&f.Version, "version", false, "Print version string") 191 | flag.BoolVar(&f.Version, "V", false, "Print version string") 192 | // Print debug info 193 | flag.BoolVar(&f.Debug, "debug", false, "Print detailed config") 194 | // Memory usage 195 | flag.BoolVar(&f.MemInfo, "meminfo", false, "Print memory info") 196 | // Refresh remailer stats files 197 | flag.BoolVar(&f.Refresh, "refresh", false, "Refresh remailer stats files") 198 | 199 | flag.Parse() 200 | return f 201 | } 202 | 203 | // findConfig attempts to locate a yamn config file 204 | func (f *Flags) findConfig() (string, error) { 205 | var err error 206 | var cfgFile string 207 | // if a --config flag was passed, try that as the highest priority 208 | if _, err = os.Stat(f.Config); err == nil { 209 | return f.Config, nil 210 | } 211 | // Does the environment variable YAMNCFG point to a valid file? 212 | if _, err = os.Stat(os.Getenv("YAMNCFG")); err == nil { 213 | return os.Getenv("YAMNCFG"), nil 214 | } 215 | // Is there a yamn.yml in the PWD? 216 | pwd, err := os.Getwd() 217 | if err == nil { 218 | cfgFile = path.Join(pwd, "yamn.yml") 219 | if _, err = os.Stat(cfgFile); err == nil { 220 | return cfgFile, nil 221 | } 222 | } 223 | // Is there a yamn.yml file in the dir flag directory 224 | cfgFile = path.Join(f.Dir, "yamn.yml") 225 | if _, err = os.Stat(cfgFile); err == nil { 226 | return cfgFile, nil 227 | } 228 | // Look for a yamn.yml in the user's homedir 229 | home, err := os.UserHomeDir() 230 | if err == nil { 231 | cfgFile = path.Join(home, "yamn.yml") 232 | if _, err = os.Stat(cfgFile); err == nil { 233 | return cfgFile, nil 234 | } 235 | } 236 | // Last gasp: Try /etc/yamn.yml. 237 | cfgFile = "/etc/yamn.yml" 238 | if _, err = os.Stat(cfgFile); err == nil { 239 | return cfgFile, nil 240 | } 241 | // Return an error to indicate no config file has been found 242 | return "", os.ErrNotExist 243 | } 244 | 245 | // newConfig returns a new instance of Config with some predefined defaults 246 | func (f *Flags) newConfig() *Config { 247 | c := new(Config) 248 | // Default values defined here will be overridden by unmarshaling a config file 249 | c.General.Loglevel = "warn" 250 | c.General.LogToFile = false // By default, log to stdout/stderr 251 | c.General.LogToJournal = false // Don't log to journal by default 252 | // Config items in the Files section default to a path defined by the --dir flag 253 | c.Files.Pubkey = path.Join(f.Dir, "key.txt") 254 | c.Files.Pubring = path.Join(f.Dir, "pubring.mix") 255 | c.Files.Secring = path.Join(f.Dir, "secring.mix") 256 | c.Files.Mlist2 = path.Join(f.Dir, "mlist2.txt") 257 | c.Files.Adminkey = path.Join(f.Dir, "adminkey.txt") 258 | c.Files.Help = path.Join(f.Dir, "help.txt") 259 | c.Files.Pooldir = path.Join(f.Dir, "pool") 260 | c.Files.Maildir = path.Join(f.Dir, "Maildir") 261 | c.Files.IDlog = path.Join(f.Dir, "idlog") 262 | c.Files.ChunkDB = path.Join(f.Dir, "chunkdb") 263 | c.Files.Logfile = path.Join(f.Dir, "yamn.log") 264 | c.Urls.Fetch = true 265 | c.Urls.Pubring = "http://www.mixmin.net/yamn/pubring.mix" 266 | c.Urls.Mlist2 = "http://www.mixmin.net/yamn/mlist2.txt" 267 | c.Mail.Sendmail = false 268 | c.Mail.Outfile = false 269 | c.Mail.SMTPRelay = "fleegle.mixmin.net" 270 | c.Mail.SMTPPort = 587 271 | c.Mail.UseTLS = true 272 | c.Mail.MXRelay = true 273 | c.Mail.OnionRelay = false // Allow .onion addresses as MX relays 274 | c.Mail.Sender = "" 275 | c.Mail.Username = "" 276 | c.Mail.Password = "" 277 | c.Mail.OutboundName = "Anonymous Remailer" 278 | c.Mail.OutboundAddy = "remailer@domain.invalid" 279 | c.Mail.CustomFrom = false 280 | c.Stats.Minrel = 98.0 281 | c.Stats.Relfinal = 99.0 282 | c.Stats.Minlat = 2 283 | c.Stats.Maxlat = 60 284 | c.Stats.Chain = "*,*,*" 285 | c.Stats.Numcopies = 1 286 | c.Stats.Distance = 2 287 | c.Stats.StaleHrs = 24 288 | c.Stats.UseExpired = false 289 | c.Pool.Size = 5 // Good for startups, too small for established 290 | c.Pool.Rate = 65 291 | c.Pool.MinSend = 5 // Only used in Binomial Mix Pools 292 | c.Pool.Loop = 300 293 | c.Pool.MaxAge = 28 294 | c.Remailer.Name = "anon" 295 | c.Remailer.Address = "mix@nowhere.invalid" 296 | c.Remailer.Exit = false 297 | c.Remailer.MaxSize = 12 298 | c.Remailer.IDexp = 14 299 | c.Remailer.ChunkExpire = 60 300 | // Discard messages if packet timestamp exceeds this age in days 301 | c.Remailer.MaxAge = 14 302 | c.Remailer.Keylife = 14 303 | c.Remailer.Keygrace = 28 304 | c.Remailer.Daemon = false 305 | return c 306 | } 307 | 308 | // ParseConfig returns an instance of Config with defaults overridden by the content of a config file 309 | func (f *Flags) ParseConfig() (*Config, error) { 310 | // Fetch an instance of Config with defaults predefined 311 | c := f.newConfig() 312 | // Try (really hard) to locate a yamn config file 313 | cfgFile, err := f.findConfig() 314 | if err == nil { 315 | // Useful for informing the user what config file is being read 316 | c.Files.Config = cfgFile 317 | yamlBytes, err := os.ReadFile(cfgFile) 318 | if err != nil { 319 | return nil, err 320 | } 321 | // Unmarshal the content of the YAML config file over the existing struct instance 322 | err = yaml.Unmarshal(yamlBytes, &c) 323 | if err != nil { 324 | return nil, err 325 | } 326 | } 327 | return c, nil 328 | } 329 | -------------------------------------------------------------------------------- /config/config_test.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "io/ioutil" 5 | "os" 6 | "testing" 7 | ) 8 | 9 | func TestFlags(t *testing.T) { 10 | expectedClient := false 11 | expectedCopies := 0 12 | f := ParseFlags() 13 | if f.Client != expectedClient { 14 | t.Errorf("Expected Client to contain \"%v\" but got \"%v\".", expectedClient, f.Client) 15 | } 16 | if f.Copies != expectedCopies { 17 | t.Errorf("Expected Client to contain \"%v\" but got \"%v\".", expectedCopies, f.Copies) 18 | } 19 | } 20 | 21 | func TestConfig(t *testing.T) { 22 | testFile, err := ioutil.TempFile("/tmp", "yamn") 23 | if err != nil { 24 | t.Fatalf("Unable to create TempFile: %v", err) 25 | } 26 | defer os.Remove(testFile.Name()) 27 | fakeCfg := `--- 28 | general: 29 | logtofile: true 30 | loglevel: info 31 | files: 32 | pubkey: /fake/dir/pubkey 33 | ` 34 | testFile.WriteString(fakeCfg) 35 | testFile.Close() 36 | f := new(Flags) 37 | // Populate the Config flag with the new testFile name. This will override any other yamn config files that might 38 | // be lurking. 39 | f.Config = testFile.Name() 40 | // act as if we've been called with --dir=/fakedir 41 | f.Dir = "/fakedir" 42 | c, err := f.ParseConfig() 43 | if err != nil { 44 | t.Fatalf("ParseConfig returned: %v", err) 45 | } 46 | // c.Files.Config is special, it returns the name of the processed YAML file 47 | if c.Files.Config != testFile.Name() { 48 | t.Errorf("expected c.Files.Config to contain \"%s\" but got \"%s\"", testFile.Name(), c.Files.Config) 49 | } 50 | // These settings are defined in the fake config 51 | if !c.General.LogToFile { 52 | t.Errorf("expected c.General.Loglevel to be true but got %v", c.General.LogToFile) 53 | } 54 | if c.General.Loglevel != "info" { 55 | t.Errorf("expected c.General.Loglevel to contain \"info\" but got \"%s\"", c.General.Loglevel) 56 | } 57 | if c.Files.Pubkey != "/fake/dir/pubkey" { 58 | t.Errorf("expected c.Files.Pubkey to contain \"/fake/dir/pubkey\" but got \"%s\"", c.Files.Pubkey) 59 | } 60 | // These settings are undefined and should return defaults 61 | if !c.Urls.Fetch { 62 | t.Errorf("expected c.Urls.Fetch to default to true but got %v", c.Urls.Fetch) 63 | } 64 | if !c.Mail.UseTLS { 65 | t.Errorf("expected c.Mail.UseTLS to default to true but got %v", c.Mail.UseTLS) 66 | } 67 | // These settings inherit defaults from flags 68 | if c.Files.IDlog != "/fakedir/idlog" { 69 | t.Errorf("Expected c.Files.IDlog to default to \"/fakedir/idlog\" but got \"%s\".", c.Files.IDlog) 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /crandom/crandom.go: -------------------------------------------------------------------------------- 1 | package crandom 2 | 3 | import ( 4 | urand "crypto/rand" 5 | "encoding/binary" 6 | "fmt" 7 | "math/rand" 8 | ) 9 | 10 | // The following section defines crypto/rand as a source for functions in 11 | // math/rand. This means we can use many of the math/rand functions with 12 | // a cryptographically random source. 13 | 14 | // cryptoRandSource is an empty struct for creating a new random source in the 15 | // rand package. 16 | type cryptoRandSource struct{} 17 | 18 | // newCryptoRandSource returns a new instance of cryptoRandSource. 19 | func newCryptoRandSource() cryptoRandSource { 20 | return cryptoRandSource{} 21 | } 22 | 23 | func (cryptoRandSource) Int63() int64 { 24 | var b [8]byte 25 | urand.Read(b[:]) 26 | return int64(binary.LittleEndian.Uint64(b[:]) & (1<<63 - 1)) 27 | } 28 | 29 | func (cryptoRandSource) Seed(_ int64) {} 30 | 31 | // And so ends the random magic section 32 | 33 | // Randbytes returns n Bytes of random data 34 | func Randbytes(n int) (b []byte) { 35 | b = make([]byte, n) 36 | read, err := urand.Read(b) 37 | if err != nil { 38 | panic(err) 39 | } 40 | if read != n { 41 | err = fmt.Errorf( 42 | "Insufficient entropy. Wanted=%d, Got=%d", 43 | n, 44 | read, 45 | ) 46 | panic(err) 47 | } 48 | return 49 | } 50 | 51 | // Dice returns a random integer of range 0-255 52 | func Dice() int { 53 | var b [1]byte 54 | urand.Read(b[:]) 55 | return int(b[0]) 56 | } 57 | 58 | // RandomInt returns an integer between 0 and max 59 | func RandomInt(max int) int { 60 | r := rand.New(newCryptoRandSource()) 61 | return r.Intn(max) 62 | } 63 | 64 | // RandInts returns a randomly ordered slice of ints 65 | func RandInts(n int) (m []int) { 66 | r := rand.New(newCryptoRandSource()) 67 | m = make([]int, n) 68 | var j int 69 | for i := 0; i < n; i++ { 70 | if i == 0 { 71 | j = 0 72 | } else { 73 | j = r.Intn(i) 74 | } 75 | m[i] = m[j] 76 | m[j] = i 77 | } 78 | return 79 | } 80 | 81 | // Shuffle performs an inline Fisher-Yates Shuffle of a string slice 82 | func Shuffle(slice []string) { 83 | r := rand.New(newCryptoRandSource()) 84 | sliceLen := len(slice) 85 | for i := range slice { 86 | j := r.Intn(sliceLen) 87 | slice[i], slice[j] = slice[j], slice[i] 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /crandom/crandom_test.go: -------------------------------------------------------------------------------- 1 | package crandom 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | // makeRange creates a slice of integers in numeric order between min and max (inclusive) 8 | func makeRange(min, max int) []int { 9 | a := make([]int, max-min+1) 10 | for i := range a { 11 | a[i] = min + i 12 | } 13 | return a 14 | } 15 | 16 | // contains tests if an integer is a member of a slice 17 | func contains(s []int, e int) bool { 18 | for _, a := range s { 19 | if a == e { 20 | return true 21 | } 22 | } 23 | return false 24 | } 25 | 26 | // sum totals all the integers in a slice 27 | func sum(s []int) (t int) { 28 | for _, i := range s { 29 | t += i 30 | } 31 | return 32 | } 33 | 34 | // sumAsc totals all the bytes in a subset(r) of a slice of strings. 35 | func sumAsc(asc []string, r int) (t int) { 36 | for _, s := range asc[0:r] { 37 | chars := []byte(s) 38 | for _, c := range chars { 39 | t += int(c) 40 | } 41 | } 42 | return 43 | } 44 | 45 | func TestDice(t *testing.T) { 46 | i := Dice() 47 | if i < 0 || i > 255 { 48 | t.Fatalf("Dice should return 0-255. Got: %d", i) 49 | } 50 | } 51 | 52 | func TestInt(t *testing.T) { 53 | for i := 0; i < 100; i++ { 54 | i := RandomInt(3) 55 | if i < 0 || i > 2 { 56 | t.Fatalf("Random integer out of range: %d", i) 57 | } 58 | } 59 | } 60 | 61 | func TestRandInts(t *testing.T) { 62 | testLen := 50 63 | is := RandInts(testLen) 64 | seq := makeRange(0, testLen-1) 65 | if len(is) != len(seq) { 66 | t.Fatalf("Incorrect sequence length. Expected=%d, Got=%d", len(seq), len(is)) 67 | } 68 | if sum(is) != sum(seq) { 69 | t.Fatalf("Sum of sequence not equal to sum of randomized slice. Expected=%d, Got=%d", sum(seq), sum(is)) 70 | } 71 | inSeq := 0 72 | for i := range seq { 73 | if !contains(is, i) { 74 | t.Fatalf("Random range does not include element: %d", i) 75 | } 76 | if is[i] == i { 77 | inSeq++ 78 | } 79 | if inSeq > testLen/10 { 80 | t.Errorf("Suspiciously high number of sequence n in slice n. Collisions=%d", inSeq) 81 | } 82 | } 83 | } 84 | 85 | func TestShuffle(t *testing.T) { 86 | start := 48 87 | end := 122 88 | shuffleText := make([]string, end-start+1) 89 | for n := 0; n <= end-start; n++ { 90 | shuffleText[n] = string(rune(n + start)) 91 | } 92 | // Compare the total byte values of all the chars before and after a shuffle. They should match. 93 | preSum := sumAsc(shuffleText, len(shuffleText)) 94 | Shuffle(shuffleText) 95 | postSum := sumAsc(shuffleText, len(shuffleText)) 96 | if preSum != postSum { 97 | t.Fatalf("Mismatched sum of acsii chars before and after shuffle. Before=%d, After=%d", preSum, postSum) 98 | } 99 | // Compare the total byte values of all the chars in a subsection of the slice. They should not match. 100 | preSum = sumAsc(shuffleText, 10) 101 | Shuffle(shuffleText) 102 | postSum = sumAsc(shuffleText, 10) 103 | if preSum == postSum { 104 | t.Fatal("Bad shuffle. Substring match in shuffled text.") 105 | } 106 | } 107 | 108 | func TestRandBytes(t *testing.T) { 109 | sampleLength := 100 110 | rb := Randbytes(sampleLength) 111 | if len(rb) != sampleLength { 112 | t.Fatalf("Mismatched byte count. Expected=%d, Got=%d", sampleLength, len(rb)) 113 | } 114 | // Count the occurances of each byte in random data. Fail if too many collisions occur. 115 | threshold := (sampleLength / 100) * 5 // 5% of whole sample 116 | m := make(map[byte]int) 117 | for _, b := range rb { 118 | m[b]++ 119 | if m[b] > threshold { 120 | t.Errorf("Suspiciously high number of matching bytes in random data. Byte=%d, Count=%d", b, m[b]) 121 | } 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /crypto.go: -------------------------------------------------------------------------------- 1 | // aesgcm provides authenticated symmetric encryption using AES-GCM. It 2 | // generates random nonces for each message, and prepends the nonce to 3 | // the ciphertext. 4 | package main 5 | 6 | import ( 7 | "crypto/aes" 8 | "crypto/cipher" 9 | ) 10 | 11 | // aesGcmEncrypt applies the necessary padding to the message and encrypts it 12 | // with AES-GCM. 13 | func aesGcmEncrypt(plain, key, nonce, data []byte) (ct []byte) { 14 | block, err := aes.NewCipher(key) 15 | if err != nil { 16 | panic(err) 17 | } 18 | 19 | gcm, err := cipher.NewGCM(block) 20 | if err != nil { 21 | panic(err) 22 | } 23 | 24 | ct = gcm.Seal(nil, nonce, plain, data) 25 | return 26 | } 27 | 28 | // aesGcmDecrypt decrypts the message and removes any padding. 29 | func aesGcmDecrypt(ct, key, nonce, data []byte) (plain []byte) { 30 | block, err := aes.NewCipher(key) 31 | if err != nil { 32 | panic(err) 33 | } 34 | 35 | gcm, err := cipher.NewGCM(block) 36 | if err != nil { 37 | panic(err) 38 | } 39 | 40 | plain, err = gcm.Open(nil, nonce, ct, data) 41 | if err != nil { 42 | panic(err) 43 | } 44 | return 45 | } 46 | 47 | // aesCtr does encrypt and decrypt of aes_ctr mode 48 | func aesCtr(in, key, iv []byte) (out []byte) { 49 | block, err := aes.NewCipher(key) 50 | if err != nil { 51 | panic(err) 52 | } 53 | out = make([]byte, len(in)) 54 | stream := cipher.NewCTR(block, iv) 55 | stream.XORKeyStream(out, in) 56 | return 57 | } 58 | -------------------------------------------------------------------------------- /doc/yamn.1: -------------------------------------------------------------------------------- 1 | .TH yamn 1 "21 Nov 2014" "version 0.1d" 2 | 3 | .SH NAME 4 | yamn - An anonymous remailer application 5 | 6 | .SH SYNOPSIS 7 | .B [-m] [-s] [-l rem1,rem2,rem3,...] [-t user@host] [-s subject] [-c num] [recipient] filename 8 | .PP 9 | .B [-M] [-D] [-s] 10 | 11 | .SH DESCRIPTION 12 | Yamn (Yet another Mix network) is an anonymous remailer. Remailers provide 13 | protection against traffic analysis and allow sending mail anonymously 14 | or pseudonymously. 15 | 16 | In the non-interactive mode, Yamn reads a message from its standard input 17 | or from a Maildir. Destination address and input file can be specified in the 18 | command line. If no address is given in the command line, the input file 19 | is expected to contain a message complete with mail headers. 20 | 21 | .SH OPTIONS 22 | .TP 23 | .B "-c, --copies=\fInum" 24 | When operating in client mode, define how many copies of each message should be 25 | sent. Multiple copies share the same exit-remailer which retains a list of 26 | processed Packet-IDs, ensuring that only a single copy is delivered. This setting overrides the 27 | default config 28 | .B "Stats/Numcopies" 29 | option (default: 1). 30 | .TP 31 | .B "--config=\fIfilename" 32 | Specify an alternate configuration file. This flag overrides the YAMNCFG 33 | environment variable which, in turn, overrides the default which expects a 34 | yamn.cfg file to exist in the same directory as the yamn binary. 35 | .TP 36 | .B "-D, --daemon" 37 | Start a remailer in an endless loop of reading, processing and sending 38 | messages. This option only has meaning when used with the 39 | .B "-M" 40 | option. 41 | .TP 42 | .B "-l, --chain=\fIrem1,rem2,rem3,..." 43 | Use the defined chain to route the message through the Yamn network. Random 44 | nodes can be selected with asterisks. E.g. --chain="*,*,*". 45 | If multiple copies are specified, all copies must share a common exit remailer. 46 | .TP 47 | .B "-m, --mail" 48 | Operate in client mode and inject a plain-text message into the Yamn pool 49 | .TP 50 | .B "-M, --remailer" 51 | Operate in server mode. Normally used in combination with 52 | .B "-D" 53 | to continuously process, otherwise only a single iteration of read, process, 54 | send will be performed. 55 | .TP 56 | .B "-R, --read-mail" 57 | Read the message from the STDIN pipe instead of from a file or Maildir. 58 | .TP 59 | .B "-s, --subject=\fIsubject" 60 | Specify a Subject header for the message. If this isn't defined, the Subject 61 | is expected to be defined within the message. 62 | .TP 63 | .B "-S, --send" 64 | Flush the outbound pool. Useful for client mode and remailer testing but 65 | should not be used on an in-production remailer. 66 | .TP 67 | .B "--stdout" 68 | Pipe the output message to STDOUT instead of storing it in the Pool. 69 | .TP 70 | .B "-t, --to=\fIuser@host" 71 | Specify a recipient for the message. If this option isn't defined, the recipient 72 | is expected to be included in the message itself. 73 | .SH CONFIGURATION 74 | Yamn, by default, reads its configuration from the file 75 | .B "yamn.cfg" 76 | in its working directory. This behaviour can be overridden using the 77 | .I "--config" 78 | option defined above. The file consists of 79 | .I "name=value" 80 | pairs, grouped into sections. 81 | .SS Files section: 82 | .TP 83 | .B "Pubring" 84 | Path to the public keyring file. Default: 85 | .BR pubring.mix . 86 | .TP 87 | .B "Mlist2" 88 | Path to the remailer statistics file. Default: 89 | .BR mlist2.txt . 90 | .TP 91 | .B "Pubkey" 92 | Path to the remailer's public key file. Default: 93 | .BR key.txt . 94 | .TP 95 | .B "Secring" 96 | Path to the remailer's secret keyring file. Default: 97 | .BR secring.mix . 98 | .TP 99 | .B "Secnew" 100 | Path to the remailer-generate secret keyring file. This file is updated as new 101 | keys are generated and old keys expire. If the operator deems fit, the Secring 102 | file can be overwritten by this constantly revised version. Default: 103 | .BR "secring.new" . 104 | .TP 105 | .B "Adminkey" 106 | Path to the operator's personal PGP Public Key. If this file exists, it will be 107 | sent in response to client remailer-adminkey requests. Default: 108 | .BR adminkey.txt . 109 | .TP 110 | .B "Help" 111 | Path to the remailer help file. This will be sent in response to a 112 | remailer-help request. Default: 113 | .BR "help.txt" . 114 | .TP 115 | .B "Pooldir" 116 | Path to the directory that will be used for storing inbound and outbound 117 | remailer messages. Default: 118 | .BR "pool" . 119 | .TP 120 | .B "Maildir" 121 | Path to the Maildir folder the remailer will read inbound messages from. 122 | Default: 123 | .BR "Maildir" . 124 | .TP 125 | .B "IDLog" 126 | Path to the directory that will host the ID Log Database. Default: 127 | .BR "idlog" . 128 | .TP 129 | .B "ChunkDB" 130 | Path to the director hosting the Chunk Database. Default: 131 | .BR "chunkdb" . 132 | .SS Urls section: 133 | Yamn has the capability to pull stats and key sources from URLs published by 134 | pingers. The following settings determine which source URLS should be used 135 | if periodic downloading is required. 136 | .TP 137 | .B "Fetch" 138 | Should Yamn attempt to retrieve stats/keys at periodic intervals? Default: 139 | .BR "yes" 140 | .TP 141 | .B "Pubring" 142 | URL of a Public Keyring source. See also 143 | .IR "Files/Pubring" 144 | for the default save location of the downloaded file. Default: 145 | .BR http://www.mixmin.net/yamn/pubring.mix . 146 | .TP 147 | .B "Mlist2" 148 | URL of an Mlist2 style stats source. See also 149 | .IR "Files/Mlist2" 150 | for the default save location of the download files. Default: 151 | .BR http://www.mixmin.net/yamn/mlist2.txt . 152 | .SS Mail section 153 | Special attention should be paid to this section. Without a knowledge of how 154 | to send outbound email, both clients and remailers cannot function. 155 | .TP 156 | .B "Outfile" 157 | If set to "yes", outbound messages will be written to the queue directory 158 | instead of mailing them. This overrides all other delivery options. Default: 159 | .BR No . 160 | .TP 161 | .B "Sendmail" 162 | Boolean (yes/no) option to determine if Yamn's internal sendmail function 163 | should be used to deliver messages. If set to yes, the settings 164 | .IR SMTPRelay , 165 | .I SMTPUsername 166 | and 167 | .I SMTPPassword 168 | .B MUST 169 | be defined. 170 | .IP 171 | If set to no, the server will to use standard SMTP relay instead. This is 172 | potentially a good option if delivering mail to a local MTA or any one where 173 | authentication is not required. The 174 | .I SMTPRelay 175 | option defines where messages should be relayed to. 176 | .TP 177 | .B SMTPRelay 178 | Hostname that mail should be relayed to. Default: 179 | .BR "127.0.0.1" . 180 | .TP 181 | .B SMTPPort 182 | Used in combination with 183 | .I SMTPRelay 184 | to define the port number of the SMTP service. Default: 185 | .BR 25 . 186 | .TP 187 | .B "Pipe" 188 | If this option is specified, messages will be piped to an external command 189 | for mail delivery. A common example being: 190 | .IR "/usr/sbin/sendmail -t" . 191 | This overrides the 192 | .I Sendmail 193 | option described above. 194 | Default: None 195 | .TP 196 | .B EnvelopeSender 197 | The sender address the remailer will use on outbound messages. This applies 198 | to all inter-remailer messages and to final-recipient messages if no 199 | user-defined sender is specified. Default: 200 | .BR "nobody@nowhere.invalid" . 201 | .SS Stats section 202 | .TP 203 | .B Minrel 204 | When constructing chains with random nodes, only those exceeding this minimum 205 | percentage uptime will be considered. Default: 206 | .BR "98.0" 207 | .TP 208 | .B Relfinal 209 | When constructing chains with a random exit node, only those exceeding this minimum 210 | percentage uptime will be considered for the final hop. Default: 211 | .BR "99.0" 212 | .TP 213 | .B Minlat 214 | The minimal latency (in minutes) the local remailer or client will consider 215 | when building a chain that contains one or more random nodes. Default: 216 | .BR "2" 217 | .TP 218 | .B Maxlat 219 | The highest latency (in minutes) the local remailer or client will consider 220 | when building a chain that contains one or more random nodes. Default: 221 | .BR "60" 222 | 223 | -------------------------------------------------------------------------------- /examples/yamn.cfg.client: -------------------------------------------------------------------------------- 1 | # This file provides a minimal client configuration. 2 | 3 | [Mail] 4 | SMTPRelay = your.mail.server 5 | smtpusername = user@your.mail.server 6 | smtppassword = password 7 | -------------------------------------------------------------------------------- /examples/yamn.cfg.remailer: -------------------------------------------------------------------------------- 1 | # This sample configuration file contains details of the principle Yamn 2 | # configuration options. Where a value is shown to the right of the '=', it 3 | # represents the default setting. 4 | 5 | [Mail] 6 | # Hostname of the SMTP server you use for outbound email. 7 | ;SMTPRelay = 8 | 9 | # Port number of your SMTP server. 10 | ;smtpport = 25 11 | 12 | # Username and password for authentication to your SMTP server. 13 | ;smtpusername = 14 | ;smtppassword = 15 | 16 | # Sendmail determines if the server tries to use Sendmail to connect securely 17 | # to a (usually) remote mailserver. If set to false, it will try to relay 18 | # messages via the defined SMTP Relay. Remailers with local MTAs will probably 19 | # want to set this to false. 20 | ;Sendmail = true 21 | 22 | [Stats] 23 | # Maximum latency accepted during random chain selection 24 | ;maxlat = 60 25 | 26 | # Minimum latency accepted during random chain selection 27 | ;minlat = 2 28 | 29 | # Minimum reliability of nodes in randomly selected chains 30 | ;minrel = 98.0 31 | 32 | # Minimum reliability of a random final hop in a chain 33 | ;relfinal = 99.0 34 | 35 | [Pool] 36 | # Number of messages that must reside in the pool before processing is 37 | # triggered. New remailers may wish to set this much lower until there is 38 | # sufficient traffic to meet higher triggers. 39 | ;Size = 45 40 | 41 | # What percentage of pool messages to process after each trigger. 42 | ;Rate = 65 43 | 44 | # Loop dictates how long (in seconds) the server will wait between checking the 45 | # pool to see if any messages need processing. This only has significance when 46 | # running in daemon mode. 47 | ;Loop = 300 48 | 49 | [Files] 50 | # Maildir directory to scan for incoming messages. If not set, the default 51 | # is to look for a /Maildir directory under the directory where the binary 52 | # resides. 53 | ;Maildir = 54 | 55 | # Location of the public keyring file. By default, in the same dir as the 56 | # binary. 57 | ;Pubring = "/home/echolot/echolot/results/pubring.mix" 58 | 59 | # Location of the published stats file (in mlist2.txt format). This file is 60 | # only required for random selection of nodes in chains. 61 | ;Mlist2 = "/home/echolot/echolot/results/mlist2.txt" 62 | 63 | [Remailer] 64 | # Remailer shortname that shows up in keyrings and stats. 65 | ;Name = "anon" 66 | 67 | # Remailer email address. This has to be a real address. 68 | ;Address = nobody@nowhere.invalid 69 | 70 | # If set to True, this remailer will deliver messages to final recipients. 71 | ;Exit = false 72 | 73 | # Daemon dictates if a started remailer performs a single pool process or runs 74 | # until terminated. 75 | ;Daemon = false 76 | 77 | # Loglevel can be 'trace', 'info', 'warn' or 'error' 78 | ;Loglevel = "info" 79 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/crooks/yamn 2 | 3 | go 1.16 4 | 5 | require ( 6 | github.com/Masterminds/log-go v0.4.0 7 | github.com/crooks/jlog v0.0.0-20211114164956-865f9c8ec45f 8 | github.com/dchest/blake2s v1.0.0 9 | github.com/golang/snappy v0.0.4 // indirect 10 | github.com/luksen/maildir v0.0.0-20210101204218-7ed7afdce6bf 11 | github.com/syndtr/goleveldb v1.0.0 12 | golang.org/x/crypto v0.0.0-20220128200615-198e4374d7ed 13 | golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27 // indirect 14 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b 15 | ) 16 | 17 | replace github.com/Masterminds/log-go v0.4.0 => github.com/crooks/log-go v0.4.1 18 | 19 | replace github.com/coreos/go-systemd => github.com/coreos/go-systemd/v22 v22.3.2 20 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 2 | github.com/coreos/go-systemd/v22 v22.3.2 h1:D9/bQk5vlXQFZ6Kwuu6zaiXJ9oTPe68++AzAJc1DzSI= 3 | github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= 4 | github.com/crooks/jlog v0.0.0-20211114164956-865f9c8ec45f h1:V6BMEgw7qb/up7WJ3Fh4hO0CtKDHdW0sABPtGCiij3Q= 5 | github.com/crooks/jlog v0.0.0-20211114164956-865f9c8ec45f/go.mod h1:TOBH+z8pAjVypNzmyOq62anS1p898fxdZr0PSWyHfDw= 6 | github.com/crooks/log-go v0.4.1 h1:YVUpOxLAwCW0DWcxE6g4xEeMhODljWyNmGG09a/aGlw= 7 | github.com/crooks/log-go v0.4.1/go.mod h1:xPgU2yZAUbQCkvzB2I8OaqV5OFqOmq907g++YvJDL5Y= 8 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 9 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 10 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 11 | github.com/dchest/blake2s v1.0.0 h1:gHCBR8ecSImY/Nwk7X0Q2KJAJcpI/HSkUAQDi8MCP4Q= 12 | github.com/dchest/blake2s v1.0.0/go.mod h1:GrKn2Lc4hWqAwRrbneYuvZ6kugiJMrjk3HHtcJkEhbs= 13 | github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM= 14 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 15 | github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= 16 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 17 | github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= 18 | github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= 19 | github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= 20 | github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= 21 | github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= 22 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 23 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 24 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 25 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 26 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 27 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 28 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 29 | github.com/luksen/maildir v0.0.0-20210101204218-7ed7afdce6bf h1:ThDitDpoyi2QxOgR9FBX7YiFApVWsg7oV/7N4nM+B0A= 30 | github.com/luksen/maildir v0.0.0-20210101204218-7ed7afdce6bf/go.mod h1:G5H0u6NGJhyvKAmheKsXTuadnQbUZIIpCvOpWDYJIKk= 31 | github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= 32 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 33 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 34 | github.com/onsi/ginkgo v1.7.0 h1:WSHQ+IS43OoUrWtD1/bbclrwK8TTH5hzp+umCiuxHgs= 35 | github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 36 | github.com/onsi/gomega v1.4.3 h1:RE1xgDvH7imwFD45h+u2SgIfERHlS2yNG4DObb5BSKU= 37 | github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= 38 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 39 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 40 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 41 | github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= 42 | github.com/sirupsen/logrus v1.7.0 h1:ShrD1U9pZB12TX0cVy0DtePoCH97K8EtX+mg7ZARUtM= 43 | github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= 44 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 45 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 46 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 47 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 48 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= 49 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 50 | github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE= 51 | github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ= 52 | go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= 53 | go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= 54 | go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= 55 | go.uber.org/zap v1.16.0/go.mod h1:MA8QOfq0BHJwdXa996Y4dYkAqRKB8/1K1QMMZVaNZjQ= 56 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 57 | golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 58 | golang.org/x/crypto v0.0.0-20220128200615-198e4374d7ed h1:YoWVYYAfvQ4ddHv3OKmIvX7NCAhFGTj62VP2l2kfBbA= 59 | golang.org/x/crypto v0.0.0-20220128200615-198e4374d7ed/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= 60 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 61 | golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= 62 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 63 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 64 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 65 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 66 | golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2 h1:CIJ76btIcR3eFI5EgSo6k1qKw9KJexJuRLI9G7Hp5wE= 67 | golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 68 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 69 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 70 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 71 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 72 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 73 | golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 74 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 75 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 76 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 77 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 78 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 79 | golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27 h1:XDXtA5hveEEV8JB2l7nhMTp3t3cHp9ZpwcdjqyEWLlo= 80 | golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 81 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 82 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 83 | golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M= 84 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 85 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 86 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 87 | golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 88 | golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 89 | golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 90 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 91 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 92 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= 93 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 94 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 95 | gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= 96 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 97 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= 98 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 99 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 100 | gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= 101 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 102 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 103 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= 104 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 105 | honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= 106 | -------------------------------------------------------------------------------- /idlog/idlog.go: -------------------------------------------------------------------------------- 1 | package idlog 2 | 3 | import ( 4 | "errors" 5 | "github.com/syndtr/goleveldb/leveldb" 6 | "time" 7 | ) 8 | 9 | type IDLog struct { 10 | db *leveldb.DB // A level DB instance 11 | validity time.Duration // Days before an entry is expired 12 | } 13 | 14 | func NewIDLog(filename string, days int) *IDLog { 15 | if days < 1 { 16 | err := errors.New("Invalid validity days. Must be 1 or more") 17 | panic(err) 18 | } 19 | iddb, err := leveldb.OpenFile(filename, nil) 20 | if err != nil { 21 | panic(err) 22 | } 23 | return &IDLog{ 24 | db: iddb, 25 | validity: time.Duration(24*days) * time.Hour, 26 | } 27 | } 28 | 29 | func (i *IDLog) Close() { 30 | i.db.Close() 31 | } 32 | 33 | // Unique tests the existance of a key and inserts if it's not there. 34 | // The data inserted is a Gob'd expiry date 35 | func (i *IDLog) Unique(key []byte) (unique bool) { 36 | var err error 37 | _, err = i.db.Get(key, nil) 38 | if err != nil { 39 | if err.Error() == "leveldb: not found" { 40 | // This condition indicates we don't know this key 41 | unique = true 42 | expireDate := time.Now().Add(i.validity) 43 | insertTimestamp, err := expireDate.GobEncode() 44 | err = i.db.Put(key, insertTimestamp, nil) 45 | if err != nil { 46 | panic(err) 47 | } 48 | } else { 49 | // It's not an error we anticipated 50 | panic(err) 51 | } 52 | } else { 53 | /* 54 | The DB already contains the key we're trying to insert. 55 | This implies that we've already processed this packet 56 | and don't want to process it again. 57 | */ 58 | unique = false 59 | } 60 | return 61 | } 62 | 63 | func (i *IDLog) Expire() (count, deleted int) { 64 | var err error 65 | now := time.Now() 66 | iter := i.db.NewIterator(nil, nil) 67 | var timestamp time.Time 68 | for iter.Next() { 69 | key := iter.Key() 70 | err = timestamp.GobDecode(iter.Value()) 71 | if err != nil { 72 | panic(err) 73 | } 74 | if now.After(timestamp) { 75 | i.db.Delete(key, nil) 76 | deleted++ 77 | } else { 78 | count++ 79 | } 80 | } 81 | iter.Release() 82 | err = iter.Error() 83 | if err != nil { 84 | panic(err) 85 | } 86 | return 87 | } 88 | -------------------------------------------------------------------------------- /keymgr/public.go: -------------------------------------------------------------------------------- 1 | package keymgr 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "encoding/hex" 7 | "fmt" 8 | "github.com/dchest/blake2s" 9 | "os" 10 | "strconv" 11 | "strings" 12 | "time" 13 | ) 14 | 15 | const ( 16 | date_format string = "2006-01-02" 17 | generatedFormat string = "Mon 02 Jan 2006 15:04:05 GMT" 18 | ) 19 | 20 | type Remailer struct { 21 | name string // Remailer Shortname 22 | Address string // Remailer Address 23 | Keyid []byte // 16 Byte Mixmaster KeyID 24 | version string // Mixmaster version 25 | caps string // Remailer capstring 26 | PK []byte // Curve25519 Public Key 27 | from time.Time // Valid-from date 28 | until time.Time // Valid until date 29 | latent int // Latency (minutes) 30 | uptime int // Uptime (10ths of a %) 31 | } 32 | 33 | type Pubring struct { 34 | pubringFile string // Pubring filename 35 | statsFile string // mlist type file 36 | useExpired bool // Consider exired keys (for Echolot) 37 | pub map[string]Remailer 38 | xref map[string]string // A cross-reference of shortnames to addresses 39 | stats bool // Have current reliability stats been imported? 40 | advertised string // The keyid a local server is currently advertising 41 | keysImported time.Time // Timestamp on most recently read pubring.mix file 42 | statsImported time.Time // Timestamp on most recently read mlist2.txt file 43 | statsGenerated time.Time // Generated timestamp on mlist2.txt file 44 | } 45 | 46 | func NewPubring(pubfile, statfile string) *Pubring { 47 | return &Pubring{ 48 | pubringFile: pubfile, 49 | statsFile: statfile, 50 | useExpired: false, 51 | pub: make(map[string]Remailer), 52 | xref: make(map[string]string), 53 | stats: false, 54 | } 55 | } 56 | 57 | // StatsStale returns true if stats are over h hours old 58 | func (p *Pubring) StatsStale(h int) bool { 59 | return p.stats && int((time.Since(p.statsGenerated).Hours())) > h 60 | } 61 | 62 | func (p *Pubring) HaveStats() bool { 63 | return p.stats 64 | } 65 | 66 | func (p *Pubring) UseExpired() { 67 | p.useExpired = true 68 | } 69 | 70 | // KeyRefresh returns True if the Pubring file has been modified 71 | func (p *Pubring) KeyRefresh() bool { 72 | stat, err := os.Stat(p.pubringFile) 73 | if err != nil { 74 | panic(err) 75 | } 76 | return stat.ModTime().After(p.keysImported) 77 | } 78 | 79 | // StatRefresh returns True if the mlist2.txt file has been modified 80 | func (p *Pubring) StatRefresh() (refresh bool) { 81 | stat, err := os.Stat(p.statsFile) 82 | if err != nil { 83 | // If there's no stats file, it's not time to refresh it 84 | return 85 | } 86 | refresh = stat.ModTime().After(p.statsImported) 87 | return 88 | } 89 | 90 | // Candidates provides a list of remailer addresses that match the specified criteria 91 | func (p Pubring) Candidates(minlat, maxlat int, minrel float32, exit bool) (c []string) { 92 | for addy := range p.pub { 93 | stats := p.pub[addy] 94 | if exit { 95 | if strings.Contains(stats.caps, "M") { 96 | // Exits are required and this is a Middle 97 | continue 98 | } 99 | } 100 | if stats.latent < minlat || stats.latent > maxlat { 101 | continue 102 | } 103 | if stats.uptime < int(minrel*10) { 104 | continue 105 | } 106 | c = append(c, addy) 107 | } 108 | return 109 | } 110 | 111 | // Count returns the number of known Public keys 112 | func (p Pubring) Count() int { 113 | return len(p.pub) 114 | } 115 | 116 | // Produces a list of public key headers 117 | func (p Pubring) KeyList() (addresses []string) { 118 | for addy := range p.pub { 119 | key := p.pub[addy] 120 | header := key.name + " " 121 | header += key.Address + " " 122 | header += hex.EncodeToString(key.Keyid) + " " 123 | header += key.version + " " 124 | header += key.caps + " " 125 | header += key.from.UTC().Format(date_format) + " " 126 | header += key.until.UTC().Format(date_format) 127 | addresses = append(addresses, header) 128 | } 129 | return 130 | } 131 | 132 | // Put inserts a new remailer struct into the Keyring 133 | func (p Pubring) Put(r Remailer) { 134 | p.pub[r.Address] = r 135 | p.xref[r.name] = r.Address 136 | } 137 | 138 | // Get returns a remailer's public info when requested by name or address 139 | func (p Pubring) Get(ref string) (r Remailer, err error) { 140 | var exists bool 141 | if strings.Contains(ref, "@") { 142 | r, exists = p.pub[ref] 143 | if !exists { 144 | err = fmt.Errorf( 145 | "%s: Remailer address not found in public "+ 146 | "keyring", 147 | ref, 148 | ) 149 | return 150 | } 151 | } else { 152 | var addy string 153 | addy, exists = p.xref[ref] 154 | if !exists { 155 | err = fmt.Errorf( 156 | "%s: Remailer name not found in public "+ 157 | "keyring", 158 | ref, 159 | ) 160 | return 161 | } 162 | r = p.pub[addy] 163 | } 164 | return 165 | } 166 | 167 | // ImportStats reads an mlist2.txt style file into a Pubring struct 168 | func (p *Pubring) ImportStats() (err error) { 169 | f, err := os.Open(p.statsFile) 170 | if err != nil { 171 | return 172 | } 173 | scanner := bufio.NewScanner(f) 174 | var remName string //Remailer name in stats 175 | var remAddr string //Remailer address from xref 176 | var lat []string //Latency hours:minutes 177 | var lathrs int //Latent Hours 178 | var latmin int //Latent Minutes 179 | var exists bool //Test for presence of remailer in xref 180 | parsePhase := 0 181 | /* Stat phases are: 182 | 0 Want Generated timestamp 183 | 1 Expecting long string of dashes 184 | 2 Expecting stats lines 185 | */ 186 | for scanner.Scan() { 187 | line := scanner.Text() 188 | switch parsePhase { 189 | case 0: 190 | // Generated: Tue 07 Oct 2014 10:50:01 GMT 191 | if strings.HasPrefix(line, "Generated: ") { 192 | p.statsGenerated, err = time.Parse(generatedFormat, line[11:]) 193 | if err != nil { 194 | err = fmt.Errorf("Failed to parse Generated date: %s", err) 195 | return 196 | } 197 | parsePhase++ 198 | } 199 | case 1: 200 | // Expecting dashes 201 | if strings.HasPrefix(line, "----------") { 202 | parsePhase++ 203 | } 204 | case 2: 205 | // Expecting stats 206 | // Splitting the line at the % sign takes out the risk 207 | // of Options being interpreted as an unpredictable 208 | // number of fields. 209 | line = strings.Split(line, "%")[0] 210 | elements := strings.Fields(line) 211 | if len(elements) == 0 { 212 | // An empty line implies we've read all stats entries 213 | parsePhase++ 214 | continue 215 | } else if len(elements) != 5 { 216 | fmt.Fprintf( 217 | os.Stderr, 218 | "Invalid stats line. Expected 7 elements, got %d\n", 219 | len(elements), 220 | ) 221 | continue 222 | } 223 | remName = elements[0] 224 | remAddr, exists = p.xref[remName] 225 | if !exists { 226 | fmt.Fprintf(os.Stderr, "%s: Stats for unknown remailer\n", remName) 227 | continue 228 | } 229 | // Element 2 is Latency in the format (hrs:mins) 230 | lat = strings.Split(elements[2], ":") 231 | if lat[0] == "" { 232 | lathrs = 0 233 | } else { 234 | lathrs, err = strconv.Atoi(lat[0]) 235 | if err != nil { 236 | fmt.Fprintf(os.Stderr, "%s: Invalid latent hours\n", remName) 237 | continue 238 | } 239 | if lathrs < 0 || lathrs > 99 { 240 | fmt.Fprintf(os.Stderr, "%s: Latent hours out of range\n", remName) 241 | continue 242 | } 243 | } 244 | latmin, err = strconv.Atoi(lat[1]) 245 | if err != nil { 246 | fmt.Fprintf(os.Stderr, "%s: Invalid latent minutes\n", remName) 247 | continue 248 | } 249 | if latmin < 0 || latmin > 59 { 250 | fmt.Fprintf(os.Stderr, "%s: Latent minutes out of range\n", remName) 251 | continue 252 | } 253 | // Element 4 is Uptime in format (xxx.xx) 254 | uptmp, err := strconv.ParseFloat(elements[4], 32) 255 | if err != nil { 256 | fmt.Fprintf(os.Stderr, "%s: Invalid uptime\n", remName) 257 | continue 258 | } 259 | if uptmp < 0 || uptmp > 100 { 260 | fmt.Fprintf(os.Stderr, "%s: Uptime out of range\n", remName) 261 | continue 262 | } 263 | tmp := p.pub[remAddr] 264 | tmp.latent = (lathrs * 60) + latmin 265 | tmp.uptime = int(uptmp * 10) 266 | p.pub[remAddr] = tmp 267 | case 3: 268 | // Reserved for future mlist2.txt processing 269 | break 270 | } 271 | } 272 | // Test that all stats phases have been achieved. 273 | if parsePhase < 3 { 274 | err = fmt.Errorf( 275 | "Unexpected EOF reading %s during phase %d", 276 | p.statsFile, 277 | parsePhase, 278 | ) 279 | return 280 | } 281 | // Update last-imported timestamp for stats 282 | stat, err := os.Stat(p.statsFile) 283 | if err != nil { 284 | panic(err) 285 | } 286 | p.statsImported = stat.ModTime() 287 | p.stats = true 288 | return 289 | } 290 | 291 | func Headers(filename string) (headers []string, err error) { 292 | var f *os.File 293 | f, err = os.Open(filename) 294 | if err != nil { 295 | return 296 | } 297 | scanner := bufio.NewScanner(f) 298 | for scanner.Scan() { 299 | line := scanner.Text() 300 | if len(strings.Split(line, " ")) == 7 { 301 | headers = append(headers, line) 302 | } 303 | } 304 | return 305 | } 306 | 307 | // ImportPubring reads a YAMN Pubring.mix file 308 | func (p *Pubring) ImportPubring() (err error) { 309 | var f *os.File 310 | f, err = os.Open(p.pubringFile) 311 | if err != nil { 312 | return 313 | } 314 | scanner := bufio.NewScanner(f) 315 | var elements []string 316 | var num_elements int 317 | var line string //Each line within Pubring.mix 318 | var rem *Remailer 319 | var pkdata []byte // Decoded Public key 320 | // The following two dates are used for validity checking. 321 | now := time.Now() 322 | key_phase := 0 323 | /* Key phases are: 324 | 0 Expecting header line 325 | 1 Expecting Begin cutmark 326 | 2 Expecting Keyid line 327 | 3 Expecting public key 328 | 4 Got End cutmark 329 | */ 330 | 331 | for scanner.Scan() { 332 | line = scanner.Text() 333 | switch key_phase { 334 | case 0: 335 | // Expecting key header line 336 | elements = strings.Split(line, " ") 337 | num_elements = len(elements) 338 | // 7 elements indicates a remailer header line in Pubring.mix 339 | if num_elements != 7 { 340 | continue 341 | } 342 | from, err := time.Parse(date_format, elements[5]) 343 | if err != nil { 344 | fmt.Fprintln(os.Stderr, "Malformed valid-from date") 345 | key_phase = 0 346 | continue 347 | } 348 | fromLastMidnight, _ := midnights(from) 349 | if now.Before(fromLastMidnight) { 350 | fmt.Fprintln(os.Stderr, elements[0]+": Key not yet valid") 351 | key_phase = 0 352 | continue 353 | } 354 | until, err := time.Parse(date_format, elements[6]) 355 | if err != nil { 356 | fmt.Fprintln(os.Stderr, "Malformed valid-to date") 357 | key_phase = 0 358 | continue 359 | } 360 | // Calculate the next midnight after the until date. 361 | _, untilNextMidnight := midnights(until) 362 | if !p.useExpired && now.After(untilNextMidnight) { 363 | fmt.Fprintf( 364 | os.Stderr, 365 | "Key expired: Name=%s, Key=%s, Date=%s\n", 366 | elements[0], 367 | elements[2], 368 | elements[6], 369 | ) 370 | key_phase = 0 371 | continue 372 | } 373 | rem = new(Remailer) 374 | rem.name = elements[0] 375 | rem.Keyid, err = hex.DecodeString(elements[2]) 376 | if err != nil { 377 | // keyid is not valid hex 378 | fmt.Fprintln(os.Stderr, "Keyid in header is not hex") 379 | key_phase = 0 380 | continue 381 | } 382 | rem.from = from 383 | rem.until = until 384 | rem.version = elements[3] 385 | rem.caps = elements[4] 386 | rem.Address = elements[1] 387 | key_phase = 1 388 | case 1: 389 | // Expecting Begin cutmark 390 | if line == "-----Begin Mix Key-----" { 391 | key_phase = 2 392 | } 393 | case 2: 394 | // Expecting Keyid line 395 | keyid, err := hex.DecodeString(line) 396 | if err != nil { 397 | // keyid is not valid hex 398 | fmt.Fprintln(os.Stderr, "Keyid in pubkey is not hex") 399 | key_phase = 0 400 | continue 401 | } 402 | if !bytes.Equal(keyid, rem.Keyid) { 403 | // Corrupt keyblock - header keyid doesn't match keyid in block 404 | fmt.Fprintln(os.Stderr, "Keyid in header differs from keyid in pubkey") 405 | key_phase = 0 406 | continue 407 | } 408 | key_phase = 3 409 | case 3: 410 | // Expecting Public key 411 | pkdata, err = hex.DecodeString(line) 412 | if err != nil { 413 | // Public key is not valid hex 414 | fmt.Fprintln(os.Stderr, "Unable to decode Public key") 415 | key_phase = 0 416 | continue 417 | } 418 | if len(pkdata) != 32 { 419 | fmt.Fprintln(os.Stderr, "Public key is not 32 bits") 420 | key_phase = 0 421 | continue 422 | } 423 | rem.PK = pkdata 424 | key_phase = 4 425 | case 4: 426 | // Expecting end cutmark 427 | if line == "-----End Mix Key-----" { 428 | p.Put(*rem) 429 | key_phase = 0 430 | } 431 | } // End of phases 432 | } // End of file scan loop 433 | 434 | // Set key imported timestamp 435 | stat, err := os.Stat(p.pubringFile) 436 | if err != nil { 437 | panic(err) 438 | } 439 | p.keysImported = stat.ModTime() 440 | return 441 | } 442 | 443 | // makeKeyID generates a 16 Byte KeyID based on the hash of a Public Key 444 | func makeKeyID(pub []byte) []byte { 445 | digest, err := blake2s.New(&blake2s.Config{Size: 16}) 446 | if err != nil { 447 | panic(err) 448 | } 449 | digest.Write(pub) 450 | return digest.Sum(nil) 451 | } 452 | 453 | // midnights returns the timestamp for the previous and next midnights of a 454 | // given timestamp 455 | func midnights(t time.Time) (lastMidnight, nextMidnight time.Time) { 456 | lastMidnight = time.Date(t.Year(), t.Month(), t.Day(), 457 | 0, 0, 0, 0, time.UTC) 458 | nextMidnight = time.Date(t.Year(), t.Month(), t.Day()+1, 459 | 0, 0, 0, 0, time.UTC) 460 | return 461 | } 462 | -------------------------------------------------------------------------------- /keymgr/public_test.go: -------------------------------------------------------------------------------- 1 | package keymgr 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strings" 7 | "testing" 8 | "time" 9 | ) 10 | 11 | func TestWritePubring(t *testing.T) { 12 | f, err := os.Create("pubring.mix") 13 | if err != nil { 14 | t.Fatal(err) 15 | } 16 | defer f.Close() 17 | var options string 18 | for n := 0; n < 10; n++ { 19 | counter := fmt.Sprintf("%02d", n) 20 | keyid := strings.Repeat("0", 32-len(counter)) + counter 21 | key := strings.Repeat("0", 32) + keyid 22 | if n < 4 { 23 | options = "E" 24 | } else { 25 | options = "M" 26 | } 27 | header := fmt.Sprintf( 28 | "test%02d test%02d@domain.foo %s 4:0.2a %s 2016-01-01 2100-12-31\n\n", 29 | n, 30 | n, 31 | keyid, 32 | options, 33 | ) 34 | f.WriteString(header) 35 | f.WriteString("-----Begin Mix Key-----\n") 36 | f.WriteString(keyid + "\n") 37 | f.WriteString(key + "\n") 38 | f.WriteString("-----End Mix Key-----\n\n") 39 | } 40 | } 41 | 42 | func TestWriteMlist2(t *testing.T) { 43 | f, err := os.Create("mlist2.txt") 44 | if err != nil { 45 | t.Fatal(err) 46 | } 47 | defer f.Close() 48 | f.WriteString("Stats-Version: 2.0\n") 49 | now := time.Now() 50 | gentime := now.Add(-90 * time.Minute) 51 | genstamp := fmt.Sprintf("Generated: %s\n", gentime.Format(generatedFormat)) 52 | f.WriteString(genstamp) 53 | f.WriteString("Mixmaster Latent-Hist Latent Uptime-Hist Uptime Options\n") 54 | f.WriteString(strings.Repeat("-", 64) + "\n") 55 | var options string 56 | for n := 0; n < 10; n++ { 57 | name := fmt.Sprintf("test%02d", n) 58 | lathist := strings.Repeat("0", 12) 59 | latent := fmt.Sprintf(" :%02d ", n) 60 | uphist := strings.Repeat("+", 12) 61 | uptime := fmt.Sprintf(" %3d.0%% ", 100-(n*5)) 62 | if n < 4 { 63 | options = "" 64 | } else { 65 | options = "D" 66 | } 67 | header := fmt.Sprintf( 68 | "%-12s %s %s %s %s %s\n", 69 | name, 70 | lathist, 71 | latent, 72 | uphist, 73 | uptime, 74 | options, 75 | ) 76 | f.WriteString(header) 77 | } 78 | f.WriteString("\nBroken type-I remailer chains:\n\n\n") 79 | f.WriteString("Broken type-II remailer chains:\n\n\n\n\n") 80 | f.WriteString("Remailer-Capabilities:\n\n") 81 | for n := 0; n < 10; n++ { 82 | name := fmt.Sprintf("test%02d", n) 83 | addy := fmt.Sprintf("%s@domain.foo", name) 84 | if n < 4 { 85 | options = "" 86 | } else { 87 | options = " middle" 88 | } 89 | header := fmt.Sprintf( 90 | "$remailer{\"%s\"} = \"<%s>%s\";\n", 91 | name, 92 | addy, 93 | options, 94 | ) 95 | f.WriteString(header) 96 | } 97 | 98 | } 99 | 100 | func TestImport(t *testing.T) { 101 | p := NewPubring("pubring.mix", "mlist2.txt") 102 | err := p.ImportPubring() 103 | if err != nil { 104 | t.Fatal(err) 105 | } 106 | fmt.Printf("Imported %d remailers from Public Keyring.\n", p.Count()) 107 | for _, rem := range p.KeyList() { 108 | fmt.Println(rem) 109 | } 110 | // Stats haven't been imported yet 111 | if p.HaveStats() { 112 | t.Fatal("Stats not yet imported") 113 | } 114 | err = p.ImportStats() 115 | if err != nil { 116 | t.Fatal(err) 117 | } 118 | // Stats have been imported 119 | if !p.HaveStats() { 120 | t.Fatal("Stats should have been imported") 121 | } 122 | // Test if the pubring.mix file has been modified since we imported 123 | // it. The expectation is that it won't have been. 124 | if p.KeyRefresh() { 125 | t.Fatal("Pubring should be fresh") 126 | } 127 | // Test if the mlist2.txt file has been modified since we imported 128 | // it. The expectation is that it won't have been. 129 | if p.StatRefresh() { 130 | t.Fatal("Stats should be fresh") 131 | } 132 | // Test stats are 90 minutes old, they shouldn't be stale 133 | if p.StatsStale(2) { 134 | t.Fatal("Test Stats are over 2 hours old") 135 | } 136 | } 137 | 138 | func TestCandidates(t *testing.T) { 139 | p := NewPubring("pubring.mix", "mlist2.txt") 140 | err := p.ImportPubring() 141 | if err != nil { 142 | t.Fatal(err) 143 | } 144 | err = p.ImportStats() 145 | if err != nil { 146 | t.Fatal(err) 147 | } 148 | // Middleman Remailer candidates 149 | candidates := p.Candidates(1, 8, 70.0, false) 150 | numCandidates := len(candidates) 151 | fmt.Printf("Middle candidates: %d\n", numCandidates) 152 | if numCandidates != 6 { 153 | t.Fatalf("Expected 6 Middle candidates, got %d", numCandidates) 154 | } 155 | // Exit Remailer candidates 156 | candidates = p.Candidates(1, 8, 70.0, true) 157 | numCandidates = len(candidates) 158 | fmt.Printf("Exit candidates: %d\n", numCandidates) 159 | if numCandidates != 3 { 160 | t.Fatalf("Expected 3 Exit candidates, got %d", numCandidates) 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /keymgr/secret.go: -------------------------------------------------------------------------------- 1 | package keymgr 2 | 3 | import ( 4 | "bufio" 5 | "encoding/hex" 6 | "errors" 7 | "fmt" 8 | "os" 9 | "strings" 10 | "time" 11 | ) 12 | 13 | const ( 14 | expiringDays int = 48 // Treat keys as expiring n hours before expired 15 | maxAddyLen int = 52 // Max chars in remailer address 16 | ) 17 | 18 | type secret struct { 19 | keyid []byte // keyid 20 | sk []byte // Secret Key 21 | from time.Time // Valid from 22 | until time.Time // Valid Until 23 | } 24 | 25 | type Secring struct { 26 | secringFile string // Filename of secret keyring 27 | pubkeyFile string // Public keyfile (key.txt) 28 | sec map[string]secret 29 | name string // Local remailer's name 30 | address string // Local remailer's email address 31 | myKeyid []byte // Keyid this remailer is advertising 32 | validity time.Duration // Period of key validity 33 | grace time.Duration // Period of grace after key expiry 34 | exit bool // Is this an Exit type remailer? 35 | version string // Yamn version string 36 | } 37 | 38 | // OpenAppend opens a file in Append mode and sets user-only permissions 39 | func OpenAppend(name string) (f *os.File, err error) { 40 | f, err = os.OpenFile(name, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0600) 41 | return 42 | } 43 | 44 | // OpenCreate opens a new file in Write mode and sets user-only permissions 45 | func OpenCreate(name string) (f *os.File, err error) { 46 | f, err = os.OpenFile(name, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0600) 47 | return 48 | } 49 | 50 | // NewSecring is a constructor for the Secret Keyring 51 | func NewSecring(secfile, pubkey string) *Secring { 52 | return &Secring{ 53 | secringFile: secfile, 54 | pubkeyFile: pubkey, 55 | sec: make(map[string]secret), 56 | } 57 | } 58 | 59 | // ListKeyids returns a string slice of all in-memory secret keyids 60 | func (s *Secring) ListKeyids() (keyids []string) { 61 | keyids = make([]string, 0, len(s.sec)) 62 | for k := range s.sec { 63 | keyids = append(keyids, k) 64 | } 65 | return 66 | } 67 | 68 | // SetName validates and sets the remailer name 69 | func (s *Secring) SetName(name string) { 70 | var err error 71 | l := len(name) 72 | if l < 2 || l > 12 { 73 | err = fmt.Errorf( 74 | "Remailer name must be 2 to 12 chars, not %d.", 75 | l, 76 | ) 77 | panic(err) 78 | } 79 | s.name = strings.ToLower(name) 80 | } 81 | 82 | // SetAddress validates and sets the remailer address 83 | func (s *Secring) SetAddress(addy string) { 84 | var err error 85 | l := len(addy) 86 | if l < 3 || l > maxAddyLen { 87 | err = fmt.Errorf( 88 | "Remailer address must be 2 to 52 chars, not %d.", 89 | l, 90 | ) 91 | panic(err) 92 | } 93 | index := strings.Index(addy, "@") 94 | if index == -1 { 95 | err = fmt.Errorf( 96 | "%s: Remailer address doesn't contain an @.", 97 | addy, 98 | ) 99 | panic(err) 100 | } else if index == 0 || l-index < 3 { 101 | err = fmt.Errorf("%s: Invalid remailer address.", addy) 102 | panic(err) 103 | } 104 | s.address = strings.ToLower(addy) 105 | } 106 | 107 | // SetExit defines if this is a Middle or Exit remailer 108 | func (s *Secring) SetExit(exit bool) { 109 | s.exit = exit 110 | } 111 | 112 | // SetValidity defines the time duration over which a key is deemed valid 113 | func (s *Secring) SetValidity(valid, grace int) { 114 | s.validity = time.Duration(24*valid) * time.Hour 115 | s.grace = time.Duration(24*grace) * time.Hour 116 | } 117 | 118 | // SetVersion sets the version string used on keys 119 | func (s *Secring) SetVersion(v string) { 120 | s.version = "4:" + v 121 | } 122 | 123 | // Count returns the number of secret keys in memory 124 | func (s *Secring) Count() int { 125 | return len(s.sec) 126 | } 127 | 128 | // Insert puts a new secret key into memory and returns its keyid 129 | func (s *Secring) Insert(pub, sec []byte) (keyidstr string) { 130 | var err error 131 | if len(pub) != 32 { 132 | err = fmt.Errorf( 133 | "Invalid pubkey length. Wanted=32, Got=%d", 134 | len(pub), 135 | ) 136 | panic(err) 137 | } 138 | if len(sec) != 32 { 139 | err = fmt.Errorf( 140 | "Invalid seckey length. Wanted=32, Got=%d", 141 | len(sec), 142 | ) 143 | panic(err) 144 | } 145 | key := new(secret) 146 | /* 147 | Keyids are arbitrary, they only server to link public and 148 | secret keys in a manner that enables clients to know which 149 | public key to encrypt to and servers to know which secret key 150 | to use for decryption. Using a truncated SHA256 of the public 151 | key provides the means for some, perhaps, useful validation 152 | that the client-held public key is not corrupt. 153 | */ 154 | key.keyid = makeKeyID(pub) 155 | keyidstr = hex.EncodeToString(key.keyid) 156 | // Validity dates 157 | key.from = time.Now() 158 | key.until = time.Now().Add(s.validity) 159 | // The secret key itself 160 | key.sk = sec 161 | s.sec[keyidstr] = *key 162 | return 163 | } 164 | 165 | // WritePublic writes the Public Key to disk. 166 | func (s *Secring) WritePublic(pub []byte, keyidstr string) { 167 | var err error 168 | if len(pub) != 32 { 169 | err = fmt.Errorf( 170 | "Invalid pubkey length. Wanted=32, Got=%d", 171 | len(pub), 172 | ) 173 | panic(err) 174 | } 175 | 176 | var capstring string 177 | // M = Middle, E = Exit 178 | if s.exit { 179 | capstring += "E" 180 | } else { 181 | capstring += "M" 182 | } 183 | 184 | key, exists := s.sec[keyidstr] 185 | if !exists { 186 | err = fmt.Errorf("%s: Keyid does not exist", keyidstr) 187 | panic(err) 188 | } 189 | 190 | header := s.name + " " 191 | header += s.address + " " 192 | header += keyidstr + " " 193 | header += s.version + " " 194 | header += capstring + " " 195 | header += key.from.UTC().Format(date_format) + " " 196 | header += key.until.UTC().Format(date_format) 197 | 198 | // Open the file for writing 199 | f, err := os.Create(s.pubkeyFile) 200 | if err != nil { 201 | panic(err) 202 | } 203 | defer f.Close() 204 | w := bufio.NewWriter(f) 205 | fmt.Fprintln(w, header) 206 | fmt.Fprintln(w, "") 207 | fmt.Fprintln(w, "-----Begin Mix Key-----") 208 | fmt.Fprintln(w, keyidstr) 209 | fmt.Fprintln(w, hex.EncodeToString(pub)) 210 | fmt.Fprintln(w, "-----End Mix Key-----") 211 | err = w.Flush() 212 | if err != nil { 213 | panic(err) 214 | } 215 | } 216 | 217 | // WriteSecret adds the selected secret key to the secret keyring file 218 | func (s *Secring) WriteSecret(keyidstr string) { 219 | var err error 220 | key, exists := s.sec[keyidstr] 221 | if !exists { 222 | err = fmt.Errorf("%s: Keyid does not exist", keyidstr) 223 | panic(err) 224 | } 225 | f, err := OpenAppend(s.secringFile) 226 | if err != nil { 227 | panic(err) 228 | } 229 | defer f.Close() 230 | keydata := "\n-----Begin Mixmaster Secret Key-----\n" 231 | keydata += fmt.Sprintf( 232 | "Created: %s\n", 233 | key.from.UTC().Format(date_format), 234 | ) 235 | keydata += fmt.Sprintf( 236 | "Expires: %s\n", 237 | key.until.UTC().Format(date_format), 238 | ) 239 | keydata += keyidstr + "\n" 240 | keydata += hex.EncodeToString(key.sk) + "\n" 241 | keydata += "-----End Mixmaster Secret Key-----\n" 242 | _, err = f.WriteString(keydata) 243 | if err != nil { 244 | panic(err) 245 | } 246 | } 247 | 248 | // WriteMyKey writes the local public key to filename with current 249 | // configurtaion settings. 250 | func (s *Secring) WriteMyKey(filename string) (keyidstr string) { 251 | infile, err := os.Open(s.pubkeyFile) 252 | if err != nil { 253 | panic(err) 254 | } 255 | defer infile.Close() 256 | // Create a tmp file rather than overwriting directly 257 | outfile, err := os.Create(filename) 258 | if err != nil { 259 | panic(err) 260 | } 261 | defer outfile.Close() 262 | in := bufio.NewScanner(infile) 263 | out := bufio.NewWriter(outfile) 264 | var line string 265 | for in.Scan() { 266 | line = in.Text() 267 | elements := strings.Fields(line) 268 | if len(elements) == 7 { 269 | var capstring string 270 | // M = Middle, E = Exit 271 | if s.exit { 272 | capstring += "E" 273 | } else { 274 | capstring += "M" 275 | } 276 | // Extract the keyid so we can return it 277 | keyidstr = elements[2] 278 | if len(keyidstr) != 32 { 279 | err = fmt.Errorf( 280 | "Invalid public keyid length. Expected=32, Got=%d.", 281 | len(keyidstr), 282 | ) 283 | panic(err) 284 | } 285 | header := s.name + " " 286 | header += s.address + " " 287 | header += keyidstr + " " 288 | header += s.version + " " 289 | header += capstring + " " 290 | header += elements[5] + " " 291 | header += elements[6] 292 | fmt.Fprintln(out, header) 293 | } else { 294 | fmt.Fprintln(out, line) 295 | } 296 | } 297 | err = out.Flush() 298 | if err != nil { 299 | panic(err) 300 | } 301 | return 302 | } 303 | 304 | // Return the Secret struct that corresponds to the requested Keyid 305 | func (s *Secring) Get(keyid string) (sec secret, err error) { 306 | var exists bool 307 | sec, exists = s.sec[keyid] 308 | if !exists { 309 | err = fmt.Errorf( 310 | "%s: Keyid not found in secret keyring", 311 | keyid, 312 | ) 313 | return 314 | } 315 | return 316 | } 317 | 318 | // Return the Secret Key that corresponds to the requested Keyid 319 | func (s *Secring) GetSK(keyid string) (sk []byte, err error) { 320 | sec, exists := s.sec[keyid] 321 | if !exists { 322 | err = fmt.Errorf( 323 | "%s: Keyid not found in secret keyring", 324 | keyid, 325 | ) 326 | return 327 | } 328 | sk = sec.sk 329 | return 330 | } 331 | 332 | // Purge deletes expired keys and writes current ones to a backup secring 333 | func (s *Secring) Purge() (active, expiring, expired, purged int) { 334 | /* 335 | Keys exist in four possible states:- 336 | 337 | active - Keys that are valid and not yet expiring 338 | expiring - Active keys that will expire soon 339 | expired - Expired keys, not yet purged from the Secring 340 | purged - Keys that have just been deleted from the Secring 341 | */ 342 | 343 | // Running Purge with undefined validity parameters would probably 344 | // trash the Secret Keyring. This tests prevents that unfortunate 345 | // circumstance. 346 | if s.validity == 0 || s.grace == 0 { 347 | err := errors.New("Cannot purge without validity parameters") 348 | panic(err) 349 | } 350 | 351 | // Rename the secring file to a tmp name, just in case this screws up. 352 | err := os.Rename(s.secringFile, s.secringFile+".tmp") 353 | if err != nil { 354 | // The implication is that no secring file exists yet 355 | return 356 | } 357 | 358 | // Create a new secring file. At this point, secringFile must not 359 | // exist. 360 | f, err := OpenCreate(s.secringFile) 361 | if err != nil { 362 | panic(err) 363 | } 364 | defer f.Close() 365 | 366 | // Set the duration prior to expired that we consider "expiring". 367 | now := time.Now() 368 | expirePeriod := time.Duration(-expiringDays) * time.Hour 369 | 370 | // Iterate key and value of Secring in memory 371 | for k, m := range s.sec { 372 | purgeDate := m.until.Add(s.grace) 373 | _, purgeNextMidnight := midnights(purgeDate) 374 | if now.After(purgeNextMidnight) { 375 | // Key has expired. Purge from memory and don't write 376 | // it back to disk. 377 | delete(s.sec, k) 378 | purged++ 379 | continue 380 | } 381 | keydata := "-----Begin Mixmaster Secret Key-----\n" 382 | keydata += fmt.Sprintf( 383 | "Created: %s\n", 384 | m.from.Format(date_format), 385 | ) 386 | keydata += fmt.Sprintf( 387 | "Expires: %s\n", 388 | m.until.Format(date_format), 389 | ) 390 | keydata += hex.EncodeToString(m.keyid) + "\n" 391 | keydata += hex.EncodeToString(m.sk) + "\n" 392 | keydata += "-----End Mixmaster Secret Key-----\n\n" 393 | _, err = f.WriteString(keydata) 394 | if err != nil { 395 | panic(err) 396 | } 397 | // Key dates on keyrings are in days so a we use nextMidnight 398 | // so that keys remain valid throughout their whole last day. 399 | _, untilNextMidnight := midnights(m.until) 400 | expiringThreshold := m.until.Add(expirePeriod) 401 | _, expiringNextMidnight := midnights(expiringThreshold) 402 | if now.After(untilNextMidnight) { 403 | expired++ 404 | } else if now.After(expiringNextMidnight) { 405 | expiring++ 406 | } else { 407 | active++ 408 | } 409 | } 410 | return 411 | } 412 | 413 | // ImportSecring reads a YAML secring.mix file into memory 414 | func (s *Secring) ImportSecring() (err error) { 415 | var f *os.File 416 | f, err = os.Open(s.secringFile) 417 | if err != nil { 418 | return 419 | } 420 | defer f.Close() 421 | scanner := bufio.NewScanner(f) 422 | var line string //Each line within secring.mix 423 | var skdata []byte // Decoded secret key 424 | var keyidMapKey string // String representation of keyid to key map with 425 | var valid time.Time 426 | var expire time.Time 427 | var sec *secret 428 | now := time.Now().UTC() 429 | nextMidnight := time.Date(now.Year(), now.Month(), now.Day()+1, 0, 0, 0, 0, time.UTC) 430 | key_phase := 0 431 | /* Key phases are: 432 | 0 Expecting Begin cutmark 433 | 1 Expecting Valid-from date 434 | 2 Expecting Valid-to date 435 | 3 Expecting Keyid line 436 | 4 Expecting secret key 437 | 5 Got End cutmark 438 | */ 439 | 440 | for scanner.Scan() { 441 | line = scanner.Text() 442 | switch key_phase { 443 | case 0: 444 | // Expecting begin cutmark 445 | if line == "-----Begin Mixmaster Secret Key-----" { 446 | sec = new(secret) 447 | key_phase = 1 448 | } 449 | case 1: 450 | // Valid-from date 451 | if line[:9] == "Created: " { 452 | valid, err = time.Parse(date_format, line[9:]) 453 | if err != nil { 454 | fmt.Fprintln( 455 | os.Stderr, 456 | "Malformed Created date", 457 | ) 458 | key_phase = 0 459 | continue 460 | } 461 | } else { 462 | fmt.Fprintln( 463 | os.Stderr, 464 | "Expected Created line", 465 | ) 466 | key_phase = 0 467 | continue 468 | } 469 | if valid.After(now) { 470 | // Key is not yet valid 471 | fmt.Fprintln( 472 | os.Stderr, 473 | "Key is not valid yet", 474 | ) 475 | key_phase = 0 476 | continue 477 | } 478 | sec.from = valid 479 | key_phase = 2 480 | case 2: 481 | // Expire date 482 | if line[:9] == "Expires: " { 483 | expire, err = time.Parse(date_format, line[9:]) 484 | if err != nil { 485 | fmt.Fprintln( 486 | os.Stderr, 487 | "Malformed Expires date", 488 | ) 489 | key_phase = 0 490 | continue 491 | } 492 | } else { 493 | fmt.Fprintln( 494 | os.Stderr, 495 | "Expected Expires line", 496 | ) 497 | key_phase = 0 498 | continue 499 | } 500 | if expire.Before(nextMidnight) { 501 | // Key has expired (but we don't care) 502 | } 503 | sec.until = expire 504 | key_phase = 3 505 | case 3: 506 | if len(line) != 32 { 507 | // Invalid keyid length 508 | key_phase = 0 509 | continue 510 | } 511 | var keyid []byte 512 | keyid, err = hex.DecodeString(line) 513 | if err != nil { 514 | // Non hex keyid 515 | fmt.Fprintln(os.Stderr, err) 516 | key_phase = 0 517 | continue 518 | } 519 | sec.keyid = keyid 520 | // Retain a textual representation to key the secring 521 | // map with 522 | keyidMapKey = line 523 | key_phase = 4 524 | case 4: 525 | // Expecting Private key 526 | skdata, err = hex.DecodeString(line) 527 | if err != nil { 528 | // Non hex Private key 529 | fmt.Fprintln(os.Stderr, err) 530 | key_phase = 0 531 | } 532 | if len(skdata) != 32 { 533 | fmt.Fprintln(os.Stderr, "Incorrect key length") 534 | key_phase = 0 535 | continue 536 | } 537 | sec.sk = skdata 538 | key_phase = 5 539 | case 5: 540 | // Expecting end cutmark 541 | if line == "-----End Mixmaster Secret Key-----" { 542 | // Add the key to the Keyring 543 | s.sec[keyidMapKey] = *sec 544 | key_phase = 0 545 | } 546 | } // End of switch 547 | } // End of file lines loop 548 | return 549 | } 550 | -------------------------------------------------------------------------------- /linebreaker/linebreaker.go: -------------------------------------------------------------------------------- 1 | package linebreaker 2 | 3 | import ( 4 | "io" 5 | ) 6 | 7 | // lineBreaker breaks data across several lines, all of the same byte length 8 | // (except possibly the last). Lines are broken with a single '\n'. 9 | type lineBreaker struct { 10 | lineLength int 11 | line []byte 12 | used int 13 | out io.Writer 14 | haveWritten bool 15 | } 16 | 17 | func NewLineBreaker(out io.Writer, lineLength int) *lineBreaker { 18 | return &lineBreaker{ 19 | lineLength: lineLength, 20 | line: make([]byte, lineLength), 21 | used: 0, 22 | out: out, 23 | } 24 | } 25 | 26 | func (l *lineBreaker) Write(b []byte) (n int, err error) { 27 | n = len(b) 28 | 29 | if n == 0 { 30 | return 31 | } 32 | 33 | if l.used == 0 && l.haveWritten { 34 | _, err = l.out.Write([]byte{'\n'}) 35 | if err != nil { 36 | return 37 | } 38 | } 39 | 40 | if l.used+len(b) < l.lineLength { 41 | l.used += copy(l.line[l.used:], b) 42 | return 43 | } 44 | 45 | l.haveWritten = true 46 | _, err = l.out.Write(l.line[0:l.used]) 47 | if err != nil { 48 | return 49 | } 50 | excess := l.lineLength - l.used 51 | l.used = 0 52 | 53 | _, err = l.out.Write(b[0:excess]) 54 | if err != nil { 55 | return 56 | } 57 | 58 | _, err = l.Write(b[excess:]) 59 | return 60 | } 61 | 62 | func (l *lineBreaker) Close() (err error) { 63 | if l.used > 0 { 64 | _, err = l.out.Write(l.line[0:l.used]) 65 | if err != nil { 66 | return 67 | } 68 | } 69 | 70 | return 71 | } 72 | -------------------------------------------------------------------------------- /linebreaker/linebreaker_test.go: -------------------------------------------------------------------------------- 1 | package linebreaker 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "encoding/base64" 7 | "io" 8 | "testing" 9 | ) 10 | 11 | func testText() string { 12 | return `’Twas brillig, and the slithy toves 13 | Did gyre and gimble in the wabe: 14 | All mimsy were the borogoves, 15 | And the mome raths outgrabe. 16 | 17 | “Beware the Jabberwock, my son! 18 | The jaws that bite, the claws that catch! 19 | Beware the Jubjub bird, and shun 20 | The frumious Bandersnatch!” 21 | 22 | He took his vorpal sword in hand; 23 | Long time the manxome foe he sought— 24 | So rested he by the Tumtum tree 25 | And stood awhile in thought. 26 | 27 | And, as in uffish thought he stood, 28 | The Jabberwock, with eyes of flame, 29 | Came whiffling through the tulgey wood, 30 | And burbled as it came! 31 | 32 | One, two! One, two! And through and through 33 | The vorpal blade went snicker-snack! 34 | He left it dead, and with its head 35 | He went galumphing back. 36 | 37 | “And hast thou slain the Jabberwock? 38 | Come to my arms, my beamish boy! 39 | O frabjous day! Callooh! Callay!” 40 | He chortled in his joy. 41 | 42 | ’Twas brillig, and the slithy toves 43 | Did gyre and gimble in the wabe: 44 | All mimsy were the borogoves, 45 | And the mome raths outgrabe.` 46 | } 47 | 48 | // wrap64 writes a byte payload as wrapped base64 to an io.writer 49 | func wrap64(writer io.Writer, b []byte, wrap int) { 50 | breaker := NewLineBreaker(writer, wrap) 51 | b64 := base64.NewEncoder(base64.StdEncoding, breaker) 52 | b64.Write(b) 53 | b64.Close() 54 | breaker.Close() 55 | } 56 | 57 | func TestWrap(t *testing.T) { 58 | inText := []byte(testText()) 59 | buf := new(bytes.Buffer) 60 | writer := bufio.NewWriter(buf) 61 | wrap64(writer, inText, 64) 62 | writer.Flush() 63 | outText := make([]byte, base64.StdEncoding.DecodedLen(buf.Len())) 64 | n, err := base64.StdEncoding.Decode(outText, buf.Bytes()) 65 | if err != nil { 66 | t.Fatal(err) 67 | } 68 | if bytes.Compare(inText, outText[:n]) != 0 { 69 | t.Fatal("Input/Output Mismatch") 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /mail.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "crypto/tls" 6 | "fmt" 7 | "net" 8 | "net/mail" 9 | "net/smtp" 10 | "os" 11 | "os/exec" 12 | "strings" 13 | "time" 14 | 15 | "github.com/Masterminds/log-go" 16 | ) 17 | 18 | func assemble(msg mail.Message) []byte { 19 | buf := new(bytes.Buffer) 20 | for h := range msg.Header { 21 | if strings.HasPrefix(h, "Yamn-") { 22 | log.Errorf("Ignoring internal mail header in assemble phase: %s", h) 23 | } else { 24 | buf.WriteString(h + ": " + msg.Header.Get(h) + "\n") 25 | //fmt.Printf("%s: %s\n", h, msg.Header.Get(h)) 26 | } 27 | } 28 | buf.WriteString("\n") 29 | buf.ReadFrom(msg.Body) 30 | return buf.Bytes() 31 | } 32 | 33 | // headToAddy parses a header containing email addresses 34 | func headToAddy(h mail.Header, header string) (addys []string) { 35 | _, exists := h[header] 36 | if !exists { 37 | return 38 | } 39 | addyList, err := h.AddressList(header) 40 | if err != nil { 41 | log.Warnf("Failed to parse header: %s", header) 42 | } 43 | for _, addy := range addyList { 44 | addys = append(addys, addy.Address) 45 | } 46 | return 47 | } 48 | 49 | type emailAddress struct { 50 | name string 51 | domain string 52 | } 53 | 54 | // splitAddress splits an email address into its component parts 55 | func splitEmailAddress(addy string) (e emailAddress, err error) { 56 | // Email addresses must have '@' signs in them. 57 | if !strings.Contains(addy, "@") { 58 | err = fmt.Errorf("%s: Email address contains no '@'", addy) 59 | return 60 | } 61 | components := strings.Split(addy, "@") 62 | if len(components) != 2 { 63 | err = fmt.Errorf("%s: Malformed email address", addy) 64 | return 65 | } 66 | e.name = components[0] 67 | e.domain = components[1] 68 | return 69 | } 70 | 71 | // mxLookup returns the responsible MX for a given email address 72 | func mxLookup(email string) (relay string, err error) { 73 | emailParts, err := splitEmailAddress(email) 74 | if err != nil { 75 | // Failed to ascertain domain name from email address 76 | return 77 | } 78 | mxRecords, err := net.LookupMX(emailParts.domain) 79 | if err != nil { 80 | relay = emailParts.domain 81 | log.Tracef( 82 | "DNS MX lookup failed for %s. Using hostname.", 83 | emailParts.domain, 84 | ) 85 | err = nil 86 | return 87 | } 88 | for _, mx := range mxRecords { 89 | if !cfg.Mail.OnionRelay { 90 | // We don't want no onions! 91 | if strings.HasSuffix(mx.Host, ".onion.") { 92 | // Ignore the onion, find another. 93 | continue 94 | } 95 | } 96 | relay = mx.Host 97 | break 98 | } 99 | if relay == "" { 100 | // No suitable relays found, use the hostname. 101 | log.Infof( 102 | "No valid MX records found for %s. Using hostname.", 103 | emailParts.domain, 104 | ) 105 | relay = emailParts.domain 106 | return 107 | } 108 | log.Tracef( 109 | "DNS lookup: Hostname=%s, MX=%s", 110 | emailParts.domain, 111 | relay, 112 | ) 113 | return 114 | } 115 | 116 | // parseFrom takes a mail address of the format Name and validates 117 | // it. If custom From headers are not allowed, it will be tweaked to conform 118 | // with the Remailer's configuration. 119 | func parseFrom(h mail.Header) []string { 120 | from, err := h.AddressList("From") 121 | if err != nil { 122 | // The supplied address is invalid. Use defaults instead. 123 | return []string{fmt.Sprintf( 124 | "%s <%s>", 125 | cfg.Mail.OutboundName, 126 | cfg.Mail.OutboundAddy, 127 | )} 128 | } 129 | if len(from) == 0 { 130 | // The address list is empty so return defaults 131 | return []string{fmt.Sprintf( 132 | "%s <%s>", 133 | cfg.Mail.OutboundName, 134 | cfg.Mail.OutboundAddy, 135 | )} 136 | } 137 | if cfg.Mail.CustomFrom { 138 | // Accept whatever was provided (it's already been validated by 139 | // AddressList). 140 | return []string{fmt.Sprintf( 141 | "%s <%s>", 142 | from[0].Name, 143 | from[0].Address, 144 | )} 145 | } 146 | if len(from[0].Name) == 0 { 147 | return []string{fmt.Sprintf( 148 | "%s <%s>", 149 | cfg.Mail.OutboundName, 150 | cfg.Mail.OutboundAddy, 151 | )} 152 | } 153 | return []string{fmt.Sprintf( 154 | "%s <%s>", 155 | from[0].Name, 156 | cfg.Mail.OutboundAddy, 157 | )} 158 | } 159 | 160 | // Read a file from the outbound pool and mail it 161 | func mailPoolFile(filename string) (delFlag bool, err error) { 162 | // This flag implies that, by default, we don't delete pool messages 163 | delFlag = false 164 | 165 | f, err := os.Open(filename) 166 | if err != nil { 167 | log.Errorf("Failed to read file for mailing: %s", err) 168 | return 169 | } 170 | defer f.Close() 171 | 172 | msg, err := mail.ReadMessage(f) 173 | if err != nil { 174 | log.Errorf("Failed to process mail file: %s", err) 175 | // If we can't process it, it'll never get sent. Mark for delete. 176 | delFlag = true 177 | return 178 | } 179 | 180 | // Test for a Pooled Date header in the message. 181 | pooledHeader := msg.Header.Get("Yamn-Pooled-Date") 182 | if pooledHeader == "" { 183 | // Legacy condition. All current versions apply this header. 184 | log.Warn("No Yamn-Pooled-Date header in message") 185 | } else { 186 | var pooledDate time.Time 187 | pooledDate, err = time.Parse(shortdate, pooledHeader) 188 | if err != nil { 189 | log.Errorf("%s: Failed to parse Yamn-Pooled-Date: %s", filename, err) 190 | return 191 | } 192 | age := daysAgo(pooledDate) 193 | if age > cfg.Pool.MaxAge { 194 | // The message has expired. Give up trying to send it. 195 | log.Infof( 196 | "%s: Refusing to mail pool file. Exceeds max age of %d days", 197 | filename, 198 | cfg.Pool.MaxAge, 199 | ) 200 | // Set deletion flag. We don't want to retain old 201 | // messages forever. 202 | delFlag = true 203 | return 204 | } 205 | if age > 0 { 206 | log.Tracef("Mailing pooled file that's %d days old.", age) 207 | } 208 | // Delete the internal header we just tested. 209 | delete(msg.Header, "Yamn-Pooled-Date") 210 | } 211 | 212 | // Add some required headers to the message. 213 | msg.Header["Date"] = []string{time.Now().Format(rfc5322date)} 214 | msg.Header["Message-Id"] = []string{messageID()} 215 | msg.Header["From"] = parseFrom(msg.Header) 216 | sendTo := headToAddy(msg.Header, "To") 217 | sendTo = append(sendTo, headToAddy(msg.Header, "Cc")...) 218 | if len(sendTo) == 0 { 219 | err = fmt.Errorf("%s: No email recipients found", filename) 220 | // No point in repeatedly trying to resend a malformed file. 221 | delFlag = true 222 | return 223 | } 224 | // There is an assumption here that all errors from mailBytes should not 225 | // delete pool files (delFlag is false by default). 226 | err = mailBytes(assemble(*msg), sendTo) 227 | return 228 | } 229 | 230 | // Mail a byte payload to a given address 231 | func mailBytes(payload []byte, sendTo []string) (err error) { 232 | // Test if the message is destined for the local remailer 233 | log.Tracef("Message recipients are: %s", strings.Join(sendTo, ",")) 234 | if cfg.Mail.Outfile { 235 | var f *os.File 236 | filename := randPoolFilename("outfile-") 237 | log.Tracef("Writing output to %s", filename) 238 | f, err = os.Create(filename) 239 | if err != nil { 240 | log.Warnf("Pool file creation failed: %s\n", err) 241 | return 242 | } 243 | defer f.Close() 244 | _, err = f.WriteString(string(payload)) 245 | if err != nil { 246 | log.Warnf("Outfile write failed: %s\n", err) 247 | return 248 | } 249 | } else if cfg.Mail.Pipe != "" { 250 | err = execSend(payload, cfg.Mail.Pipe) 251 | if err != nil { 252 | log.Warn("Email pipe failed") 253 | return 254 | } 255 | } else if cfg.Mail.Sendmail { 256 | err = sendmail(payload, sendTo) 257 | if err != nil { 258 | log.Warn("Sendmail failed") 259 | return 260 | } 261 | } else { 262 | err = smtpRelay(payload, sendTo) 263 | if err != nil { 264 | log.Warn("SMTP relay failed") 265 | return 266 | } 267 | } 268 | return 269 | } 270 | 271 | // Pipe mail to an external command (E.g. sendmail -t) 272 | func execSend(payload []byte, execCmd string) (err error) { 273 | sendmail := new(exec.Cmd) 274 | sendmail.Args = strings.Fields(execCmd) 275 | sendmail.Path = sendmail.Args[0] 276 | 277 | stdin, err := sendmail.StdinPipe() 278 | if err != nil { 279 | log.Errorf("%s: %s", execCmd, err) 280 | return 281 | } 282 | defer stdin.Close() 283 | sendmail.Stdout = os.Stdout 284 | sendmail.Stderr = os.Stderr 285 | err = sendmail.Start() 286 | if err != nil { 287 | log.Errorf("%s: %s", execCmd, err) 288 | return 289 | } 290 | stdin.Write(payload) 291 | stdin.Close() 292 | err = sendmail.Wait() 293 | if err != nil { 294 | log.Errorf("%s: %s", execCmd, err) 295 | return 296 | } 297 | return 298 | } 299 | 300 | func smtpRelay(payload []byte, sendTo []string) (err error) { 301 | conf := new(tls.Config) 302 | //conf.CipherSuites = []uint16{tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA} 303 | conf.InsecureSkipVerify = true 304 | //conf.MinVersion = tls.VersionSSL30 305 | //conf.MaxVersion = tls.VersionTLS10 306 | relay := cfg.Mail.SMTPRelay 307 | port := cfg.Mail.SMTPPort 308 | 309 | /* 310 | The following section tries to get the MX record for the 311 | recipient email address, when there is only a single recipient. 312 | If it succeeds, the email will be sent directly to the 313 | recipient MX. 314 | */ 315 | if cfg.Mail.MXRelay && len(sendTo) == 1 { 316 | log.Tracef("DNS lookup of MX record for %s.", sendTo[0]) 317 | mx, err := mxLookup(sendTo[0]) 318 | if err == nil { 319 | log.Tracef( 320 | "Doing direct relay for %s to %s:25.", 321 | sendTo[0], 322 | mx, 323 | ) 324 | relay = mx 325 | port = 25 326 | } 327 | } 328 | serverAddr := fmt.Sprintf("%s:%d", relay, port) 329 | 330 | conn, err := net.Dial("tcp", serverAddr) 331 | if err != nil { 332 | log.Warnf("Dial Error: Server=%s, Error=%s", serverAddr, err) 333 | return 334 | } 335 | 336 | client, err := smtp.NewClient(conn, relay) 337 | if err != nil { 338 | log.Warnf( 339 | "SMTP Connection Error: Server=%s, Error=%s", 340 | serverAddr, 341 | err, 342 | ) 343 | return 344 | } 345 | // Test if the remote MTA supports STARTTLS 346 | ok, _ := client.Extension("STARTTLS") 347 | if ok && cfg.Mail.UseTLS { 348 | if err = client.StartTLS(conf); err != nil { 349 | log.Warnf( 350 | "Error performing STARTTLS: Server=%s, Error=%s", 351 | serverAddr, 352 | err, 353 | ) 354 | return 355 | } 356 | } 357 | // If AUTH is supported and a UserID and Password are configured, try to 358 | // authenticate to the remote MTA. 359 | ok, _ = client.Extension("AUTH") 360 | if ok && cfg.Mail.Username != "" && cfg.Mail.Password != "" { 361 | auth := smtp.PlainAuth( 362 | "", 363 | cfg.Mail.Username, 364 | cfg.Mail.Password, 365 | cfg.Mail.SMTPRelay, 366 | ) 367 | if err = client.Auth(auth); err != nil { 368 | log.Warnf("Auth Error: Server=%s, Error=%s", serverAddr, err) 369 | return 370 | } 371 | } 372 | // Remailer.Address is a legacy setting as clients may also need to 373 | // set the sender address if their ISPs MTA demands it's valid. 374 | // TODO remove cfg.Remailer.Address in a later version (27/04/2015) 375 | var sender string 376 | if cfg.Mail.Sender != "" { 377 | sender = cfg.Mail.Sender 378 | } else { 379 | sender = cfg.Remailer.Address 380 | } 381 | if err = client.Mail(sender); err != nil { 382 | log.Warnf("SMTP Error: Server=%s, Error=%s", serverAddr, err) 383 | return 384 | } 385 | 386 | for _, addr := range sendTo { 387 | if err = client.Rcpt(addr); err != nil { 388 | log.Warnf("Error: %s\n", err) 389 | return 390 | } 391 | } 392 | 393 | w, err := client.Data() 394 | if err != nil { 395 | log.Warnf("Error: %s\n", err) 396 | return 397 | } 398 | 399 | _, err = w.Write(payload) 400 | if err != nil { 401 | log.Warnf("Error: %s\n", err) 402 | return 403 | 404 | } 405 | 406 | err = w.Close() 407 | if err != nil { 408 | log.Warnf("Error: %s\n", err) 409 | return 410 | 411 | } 412 | 413 | client.Quit() 414 | return 415 | } 416 | 417 | // sendmail invokes go's sendmail method 418 | func sendmail(payload []byte, sendTo []string) (err error) { 419 | auth := smtp.PlainAuth( 420 | "", 421 | cfg.Mail.Username, 422 | cfg.Mail.Password, 423 | cfg.Mail.SMTPRelay) 424 | relay := fmt.Sprintf("%s:%d", cfg.Mail.SMTPRelay, cfg.Mail.SMTPPort) 425 | err = smtp.SendMail(relay, auth, cfg.Remailer.Address, sendTo, payload) 426 | if err != nil { 427 | log.Warn(err) 428 | return 429 | } 430 | return 431 | } 432 | -------------------------------------------------------------------------------- /packet.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "crypto/rand" 6 | "encoding/binary" 7 | "encoding/hex" 8 | "errors" 9 | "fmt" 10 | "strings" 11 | "time" 12 | 13 | "github.com/crooks/yamn/crandom" 14 | "github.com/dchest/blake2s" 15 | "golang.org/x/crypto/nacl/box" 16 | //"code.google.com/p/go.crypto/nacl/box" 17 | ) 18 | 19 | const ( 20 | maxChainLength = 10 21 | headerBytes = 256 // An entire header slot 22 | encHeadBytes = 160 // The encrypted component of a header 23 | encDataBytes = 64 // Exit / Intermediate header component 24 | headersBytes = headerBytes * maxChainLength 25 | encHeadersBytes = headersBytes - headerBytes 26 | bodyBytes = 17920 27 | messageBytes = headersBytes + bodyBytes 28 | ) 29 | 30 | // Generate a public/private ECC key pair 31 | func eccGenerate() (pk, sk []byte) { 32 | pka, ska, err := box.GenerateKey(rand.Reader) 33 | if err != nil { 34 | panic(err) 35 | } 36 | pk = make([]byte, 32) 37 | sk = make([]byte, 32) 38 | copy(pk[:], pka[:]) 39 | copy(sk[:], ska[:]) 40 | return 41 | } 42 | 43 | /* 44 | Slot Header Format 45 | [ Recipient key ID 16 Bytes ] 46 | [ Sender Public key 32 Bytes ] 47 | [ Xsalsa20 Nonce 24 Bytes ] 48 | [ Encrypted header 176 Bytes ] (160 + Overhead) 49 | [ Random padding 8 Bytes ] 50 | Total 256 Bytes 51 | */ 52 | type encodeHeader struct { 53 | gotRecipient bool 54 | recipientKeyID []byte 55 | recipientPK [32]byte 56 | } 57 | 58 | func newEncodeHeader() *encodeHeader { 59 | return &encodeHeader{ 60 | gotRecipient: false, 61 | recipientKeyID: make([]byte, 16), 62 | } 63 | } 64 | 65 | func (h *encodeHeader) setRecipient(recipientKeyID, recipientPK []byte) { 66 | err := lenCheck(len(recipientKeyID), 16) 67 | if err != nil { 68 | panic(err) 69 | } 70 | err = lenCheck(len(recipientPK), 32) 71 | if err != nil { 72 | panic(err) 73 | } 74 | // Copying from a slice to an array requires trickery ([:]) 75 | copy(h.recipientPK[:], recipientPK) 76 | copy(h.recipientKeyID, recipientKeyID) 77 | h.gotRecipient = true 78 | } 79 | 80 | func (h *encodeHeader) encode(encHead []byte) []byte { 81 | var err error 82 | // Test a recipient has been defined 83 | if !h.gotRecipient { 84 | err = errors.New("header encode without defining recipient") 85 | panic(err) 86 | } 87 | // Test passed encHead is the correct length 88 | err = lenCheck(len(encHead), encHeadBytes) 89 | if err != nil { 90 | panic(err) 91 | } 92 | 93 | // Every header has a randomly generated sender PK & SK 94 | senderPK, senderSK, err := box.GenerateKey(rand.Reader) 95 | if err != nil { 96 | panic(err) 97 | } 98 | var nonce [24]byte 99 | copy(nonce[:], crandom.Randbytes(24)) 100 | buf := new(bytes.Buffer) 101 | buf.Write(h.recipientKeyID) 102 | buf.Write(senderPK[:]) 103 | buf.Write(nonce[:]) 104 | buf.Write(box.Seal(nil, encHead, &nonce, &h.recipientPK, senderSK)) 105 | err = lenCheck(buf.Len(), 248) 106 | if err != nil { 107 | panic(err) 108 | } 109 | buf.Write(crandom.Randbytes(headerBytes - buf.Len())) 110 | return buf.Bytes() 111 | } 112 | 113 | type decodeHeader struct { 114 | header []byte 115 | gotRecipient bool 116 | recipientSK [32]byte 117 | } 118 | 119 | func newDecodeHeader(b []byte) *decodeHeader { 120 | err := lenCheck(len(b), headerBytes) 121 | if err != nil { 122 | panic(err) 123 | } 124 | h := new(decodeHeader) 125 | h.header = make([]byte, 256) 126 | copy(h.header, b) 127 | h.gotRecipient = false 128 | return h 129 | } 130 | 131 | // getRecipientKeyID returns the encoded keyid as a string. This is required 132 | // to ascertain the Recipient Secret Key that will be passed to 133 | // "setRecipientSK". 134 | func (h *decodeHeader) getRecipientKeyID() (keyid string) { 135 | return hex.EncodeToString(h.header[0:16]) 136 | } 137 | 138 | // setRecipientSK defines the Secret Key that will be used to decrypt the 139 | // Encrypted Header component. 140 | func (h *decodeHeader) setRecipientSK(recipientSK []byte) { 141 | err := lenCheck(len(recipientSK), 32) 142 | if err != nil { 143 | panic(err) 144 | } 145 | copy(h.recipientSK[:], recipientSK) 146 | h.gotRecipient = true 147 | } 148 | 149 | func (h *decodeHeader) decode() (data []byte, version int, err error) { 150 | if !h.gotRecipient { 151 | err = errors.New("cannot decode header until recipient defined") 152 | panic(err) 153 | } 154 | // Length to decode should be lenEndBytes plus the NaCl Box overhead 155 | var senderPK [32]byte 156 | copy(senderPK[:], h.header[16:48]) 157 | var nonce [24]byte 158 | copy(nonce[:], h.header[48:72]) 159 | data, auth := box.Open( 160 | nil, 161 | h.header[72:248], 162 | &nonce, 163 | &senderPK, 164 | &h.recipientSK, 165 | ) 166 | if !auth { 167 | err = errors.New("authentication failed decrypting slot data") 168 | return 169 | } 170 | // Version number is the first byte of decrypted data 171 | version = int(data[0]) 172 | return 173 | } 174 | 175 | /* 176 | Encrypted data 177 | [ Packet version 1 Byte ] 178 | [ Packet type ID 1 Byte ] 179 | [ Delivery protocol 1 Byte ] 180 | [ Packet ID 16 Bytes ] 181 | [ AES-CTR key 32 Bytes ] 182 | [ Timestamp 2 Bytes ] 183 | [ Packet info 64 Bytes ] 184 | [ Anti-tag digest 32 Bytes ] 185 | [ Padding 11 Bytes ] 186 | Total 160 Bytes 187 | 188 | Packet Type: 0=Intermediate 1=Exit 189 | Delivery protocol: 0=SMTP 190 | */ 191 | type slotData struct { 192 | version uint8 193 | packetType uint8 194 | protocol uint8 195 | packetID []byte 196 | gotAesKey bool // Test if the AES Key has been defined 197 | aesKey []byte // Used for encrypting slots and body 198 | timestamp []byte 199 | gotPacketInfo bool // Test if packetInfo has been defined 200 | packetInfo []byte 201 | gotTagHash bool // Test if Anti-tag hash has been defined 202 | tagHash []byte 203 | } 204 | 205 | func newSlotData() *slotData { 206 | // timestamp will contain the current days since Epoch 207 | timestamp := make([]byte, 2) 208 | ts := time.Now().UTC().Unix() / 86400 209 | // Add some randomness to the timestamp by subtracting 0-3 days 210 | ts -= int64(crandom.Dice() % 4) 211 | binary.LittleEndian.PutUint16(timestamp, uint16(ts)) 212 | return &slotData{ 213 | version: 2, // This packet format is v2 214 | packetType: 0, 215 | protocol: 0, 216 | // packetID is random for intermediate hops but needs to be 217 | // identical on multi-copy Exits. 218 | packetID: crandom.Randbytes(16), 219 | gotAesKey: false, 220 | aesKey: make([]byte, 32), 221 | timestamp: timestamp, 222 | gotPacketInfo: false, 223 | gotTagHash: false, 224 | tagHash: make([]byte, 32), 225 | } 226 | } 227 | 228 | // getPacketID returns the Packet-ID from the Slot Data. 229 | func (head *slotData) getPacketID() []byte { 230 | return head.packetID 231 | } 232 | 233 | func (head *slotData) getPacketType() int { 234 | return int(head.packetType) 235 | } 236 | 237 | // setExit overrides the default Packet Type (0 = Intermediate) with an Exit 238 | // Packet Type (Exit = 1) 239 | func (head *slotData) setExit() { 240 | head.packetType = 1 241 | } 242 | 243 | func (head *slotData) getAesKey() []byte { 244 | return head.aesKey 245 | } 246 | 247 | // setAesKey defines the AES key required to decode the header stack and body. 248 | // For Exit headers, this can be completely random, but for Intermediates, it 249 | // needs to be predetermined in order to calculate Anti-Tag hashes. 250 | func (head *slotData) setAesKey(key []byte) { 251 | err := lenCheck(len(key), 32) 252 | if err != nil { 253 | panic(err) 254 | } 255 | copy(head.aesKey, key) 256 | head.gotAesKey = true 257 | } 258 | 259 | // setPacketID overrides the random ID defined in newSlotData. This ensures 260 | // that on multi-copy messages, the exit hops all have the same Packet ID. 261 | func (head *slotData) setPacketID(id []byte) { 262 | err := lenCheck(len(id), 16) 263 | if err != nil { 264 | panic(err) 265 | } 266 | copy(head.packetID, id) 267 | } 268 | 269 | func (head *slotData) setTagHash(hash []byte) { 270 | err := lenCheck(len(hash), 32) 271 | if err != nil { 272 | panic(err) 273 | } 274 | copy(head.tagHash, hash) 275 | head.gotTagHash = true 276 | } 277 | 278 | func (head *slotData) getTagHash() []byte { 279 | return head.tagHash 280 | } 281 | 282 | func (head *slotData) setPacketInfo(ei []byte) { 283 | err := lenCheck(len(ei), encDataBytes) 284 | if err != nil { 285 | panic(err) 286 | } 287 | head.gotPacketInfo = true 288 | head.packetInfo = ei 289 | } 290 | 291 | // setTimestamp creates a two-Byte timestamp (in little Endian format) based on 292 | // the number of days since Epoch. 293 | func (head *slotData) setTimestamp() { 294 | d := uint16(time.Now().UTC().Unix() / 86400) 295 | binary.LittleEndian.PutUint16(head.timestamp, d) 296 | return 297 | } 298 | 299 | // ageTimestamp returns an integer of the timestamp's age in days. 300 | func (head *slotData) ageTimestamp() int { 301 | err := lenCheck(len(head.timestamp), 2) 302 | if err != nil { 303 | panic(err) 304 | } 305 | now := int(time.Now().UTC().Unix() / 86400) 306 | then := int(binary.LittleEndian.Uint16(head.timestamp)) 307 | return now - then 308 | } 309 | 310 | func (head *slotData) encode() []byte { 311 | if !head.gotAesKey { 312 | err := errors.New( 313 | "AES key not specified before attempt to encode " + 314 | "Encrypted Header", 315 | ) 316 | panic(err) 317 | } 318 | if !head.gotPacketInfo { 319 | err := errors.New("exit/intermediate not defined before attempt to encode encrypted header") 320 | panic(err) 321 | } 322 | if !head.gotTagHash { 323 | err := errors.New("anti-tag hash not defined before attempt to encode encrypted header") 324 | panic(err) 325 | } 326 | buf := new(bytes.Buffer) 327 | buf.WriteByte(head.version) 328 | buf.WriteByte(head.packetType) 329 | buf.WriteByte(head.protocol) 330 | buf.Write(head.packetID) 331 | buf.Write(head.aesKey) 332 | buf.Write(head.timestamp) 333 | buf.Write(head.packetInfo) 334 | buf.Write(head.tagHash) 335 | err := lenCheck(buf.Len(), 149) 336 | if err != nil { 337 | panic(err) 338 | } 339 | buf.WriteString(strings.Repeat("\x00", encHeadBytes-buf.Len())) 340 | return buf.Bytes() 341 | } 342 | 343 | func decodeSlotData(b []byte) *slotData { 344 | err := lenCheck(len(b), encHeadBytes) 345 | if err != nil { 346 | panic(err) 347 | } 348 | // Test the correct libary is being employed for the packet version 349 | version := int(b[0]) 350 | if version != 2 { 351 | err := fmt.Errorf("attempt to decode packet v%d with v2 library", version) 352 | panic(err) 353 | } 354 | return &slotData{ 355 | version: b[0], 356 | packetType: b[1], 357 | protocol: b[2], 358 | packetID: b[3:19], 359 | aesKey: b[19:51], 360 | timestamp: b[51:53], 361 | packetInfo: b[53:117], 362 | tagHash: b[117:149], 363 | } 364 | } 365 | 366 | /* 367 | Enyrypted Final 368 | [ AES-CTR IV 16 Bytes ] 369 | [ Chunk num 1 Byte ] 370 | [ Num chunks 1 Byte ] 371 | [ Message ID 16 Bytes ] 372 | [ Body length 4 Bytes ] 373 | [ Delivery method 1 Byte ] 374 | [ Padding 25 Bytes ] 375 | Total 64 Bytes 376 | 377 | Delivery methods: 0=SMTP, 255=Dummy 378 | */ 379 | type slotFinal struct { 380 | aesIV []byte 381 | chunkNum uint8 382 | numChunks uint8 383 | messageID []byte 384 | packetID []byte // Not encoded but used in Slot Header on Exits 385 | gotBodyBytes bool 386 | bodyBytes int 387 | deliveryMethod uint8 388 | } 389 | 390 | func newSlotFinal() *slotFinal { 391 | return &slotFinal{ 392 | aesIV: crandom.Randbytes(16), 393 | chunkNum: 1, 394 | numChunks: 1, 395 | messageID: crandom.Randbytes(16), 396 | packetID: crandom.Randbytes(16), 397 | gotBodyBytes: false, 398 | deliveryMethod: 0, 399 | } 400 | } 401 | 402 | func (f *slotFinal) getBodyBytes() int { 403 | return f.bodyBytes 404 | } 405 | 406 | func (f *slotFinal) setBodyBytes(length int) { 407 | if length > bodyBytes { 408 | err := fmt.Errorf("body (%d bytes) exceeds maximum (%d bytes)", length, bodyBytes) 409 | panic(err) 410 | } 411 | f.bodyBytes = length 412 | f.gotBodyBytes = true 413 | } 414 | 415 | func (f *slotFinal) getAesIV() []byte { 416 | return f.aesIV 417 | } 418 | 419 | // getPacketID returns the packet ID that should be copied into the Slot Data 420 | // for Exit Hop messages. When creating mutliple copies, the PacketID needs to 421 | // be common across all exit packets to prevent duplicate deliveries. 422 | func (f *slotFinal) getPacketID() []byte { 423 | return f.packetID 424 | } 425 | 426 | func (f *slotFinal) getNumChunks() int { 427 | return int(f.numChunks) 428 | } 429 | 430 | func (f *slotFinal) setNumChunks(n int) { 431 | f.numChunks = uint8(n) 432 | } 433 | 434 | func (f *slotFinal) getMessageID() []byte { 435 | return f.messageID 436 | } 437 | 438 | func (f *slotFinal) setDeliveryMethod(n int) { 439 | f.deliveryMethod = uint8(n) 440 | } 441 | 442 | func (f *slotFinal) getDeliveryMethod() int { 443 | return int(f.deliveryMethod) 444 | } 445 | 446 | func (f *slotFinal) getChunkNum() int { 447 | return int(f.chunkNum) 448 | } 449 | 450 | func (f *slotFinal) setChunkNum(n int) { 451 | if uint8(n) > f.numChunks { 452 | err := fmt.Errorf("attempt to set chunk num (%d) greater than defined number of chunks (%d)", n, int(f.numChunks)) 453 | panic(err) 454 | } 455 | f.chunkNum = uint8(n) 456 | } 457 | 458 | func (f *slotFinal) encode() []byte { 459 | if !f.gotBodyBytes { 460 | err := errors.New("cannot encode slot final before body length is defined") 461 | panic(err) 462 | } 463 | buf := new(bytes.Buffer) 464 | buf.Write(f.aesIV) 465 | buf.WriteByte(f.chunkNum) 466 | buf.WriteByte(f.numChunks) 467 | buf.Write(f.messageID) 468 | tmp := make([]byte, 4) 469 | binary.LittleEndian.PutUint32(tmp, uint32(f.bodyBytes)) 470 | buf.Write(tmp) 471 | buf.WriteByte(f.deliveryMethod) 472 | err := lenCheck(buf.Len(), 39) 473 | if err != nil { 474 | panic(err) 475 | } 476 | buf.WriteString(strings.Repeat("\x00", encDataBytes-buf.Len())) 477 | return buf.Bytes() 478 | } 479 | 480 | func decodeFinal(b []byte) *slotFinal { 481 | err := lenCheck(len(b), encDataBytes) 482 | if err != nil { 483 | panic(err) 484 | } 485 | return &slotFinal{ 486 | aesIV: b[:16], 487 | chunkNum: b[16], 488 | numChunks: b[17], 489 | messageID: b[18:34], 490 | bodyBytes: int(binary.LittleEndian.Uint32(b[34:38])), 491 | deliveryMethod: b[38], 492 | } 493 | } 494 | 495 | /* Encrypted Intermediate 496 | [ AES-CTR IV (Partial) 12 Bytes ] 497 | [ Next hop address 52 Bytes ] 498 | [ Padding 0 Bytes ] 499 | Total 64 Bytes 500 | 501 | IVs are: 502 | [ 9 * Header slots ] 503 | [ 1 * Deterministic header ] 504 | [ 1 * Payload header ] 505 | */ 506 | 507 | type slotIntermediate struct { 508 | gotAesIV12 bool 509 | aesIV12 []byte 510 | nextHop []byte 511 | } 512 | 513 | func newSlotIntermediate() *slotIntermediate { 514 | return &slotIntermediate{ 515 | gotAesIV12: false, 516 | aesIV12: make([]byte, 12), 517 | nextHop: make([]byte, 52), 518 | } 519 | } 520 | 521 | func (s *slotIntermediate) setPartialIV(partialIV []byte) { 522 | if len(partialIV) != 12 { 523 | err := fmt.Errorf("invalid iv input: expected 12 bytes, got %d bytes", len(partialIV)) 524 | panic(err) 525 | } 526 | s.gotAesIV12 = true 527 | copy(s.aesIV12, partialIV) 528 | } 529 | 530 | //seqIV constructs a complete 16 Byte IV from a partial 12 Byte IV + a 4 Byte 531 | //counter. 532 | func seqIV(partialIV []byte, slot int) (iv []byte) { 533 | err := lenCheck(len(partialIV), 12) 534 | if err != nil { 535 | panic(err) 536 | } 537 | iv = make([]byte, 16) 538 | copy(iv[0:4], partialIV[0:4]) 539 | copy(iv[8:16], partialIV[4:12]) 540 | ctr := make([]byte, 4) 541 | binary.LittleEndian.PutUint32(ctr, uint32(slot)) 542 | copy(iv[4:8], ctr) 543 | return 544 | } 545 | 546 | // setNextHop inserts the name of the next hop remailer and pads it. 547 | func (s *slotIntermediate) setNextHop(nh string) { 548 | if len(nh) > 52 { 549 | err := fmt.Errorf("next hop address exceeds 52 chars") 550 | panic(err) 551 | } 552 | s.nextHop = []byte(nh + strings.Repeat("\x00", 52-len(nh))) 553 | } 554 | 555 | //getNextHop returns the next hop remailer name after stripping any padding. 556 | func (s *slotIntermediate) getNextHop() string { 557 | return strings.TrimRight(string(s.nextHop), "\x00") 558 | } 559 | 560 | // AES_IV constructs a 16 Byte IV from an input of 12 random Bytes and a uint32 561 | // counter. The format is arbitrary but needs to be predictable and consistent 562 | // between encrypt and decrypt operations. 563 | func (s *slotIntermediate) seqIV(counter int) (iv []byte) { 564 | if !s.gotAesIV12 { 565 | err := errors.New("cannot sequence IV until partial IV is defined") 566 | panic(err) 567 | } 568 | // IV format is: RRRRCCCCRRRRRRRR. Where R=Random and C=Counter 569 | iv = make([]byte, 16) 570 | copy(iv, seqIV(s.aesIV12, counter)) 571 | return 572 | } 573 | 574 | func (s *slotIntermediate) encode() []byte { 575 | if !s.gotAesIV12 { 576 | err := errors.New("cannot encode until partial IV is defined") 577 | panic(err) 578 | } 579 | var err error 580 | buf := new(bytes.Buffer) 581 | buf.Write(s.aesIV12) 582 | buf.Write(s.nextHop) 583 | err = lenCheck(buf.Len(), 64) 584 | if err != nil { 585 | panic(err) 586 | } 587 | return buf.Bytes() 588 | } 589 | 590 | func decodeIntermediate(b []byte) *slotIntermediate { 591 | err := lenCheck(len(b), encDataBytes) 592 | if err != nil { 593 | panic(err) 594 | } 595 | return &slotIntermediate{ 596 | gotAesIV12: true, 597 | aesIV12: b[:12], 598 | nextHop: b[12:], 599 | } 600 | } 601 | 602 | // ----- Deterministic Headers ----- 603 | type encMessage struct { 604 | gotPayload bool // Test if a Payload has been submitted 605 | payload []byte // The actual Yamn message 606 | plainLength int // Length of the plain-text bytes 607 | keys [maxChainLength - 1][]byte 608 | ivs [maxChainLength - 1][]byte 609 | chainLength int // Number of hops in chain 610 | intermediateHops int // Number of Intermediate hops 611 | padHeaders int // Number of padding headers 612 | padBytes int // Total bytes of padding 613 | } 614 | 615 | // newEncMessage creates a new encMessage object. This provides client-side 616 | // functionality for creating a new Yamn message. 617 | func newEncMessage() *encMessage { 618 | return &encMessage{ 619 | gotPayload: false, 620 | payload: make([]byte, messageBytes), 621 | chainLength: 0, 622 | } 623 | } 624 | 625 | // getPayload returns the raw payload bytes. Currently no checks are performed 626 | // as to what state the payload is in when its requested. 627 | func (m *encMessage) getPayload() []byte { 628 | return m.payload 629 | } 630 | 631 | // setChainLength takes an integer containing the length of the chain being 632 | // encoded. From this we can derive various settings, such as the number of 633 | // fake (padding) headers required. It also initializes and populates an array 634 | // of AES keys and IVs used to encrypt the intermediate hops. These have to be 635 | // predefined as they're required to create deterministic headers. 636 | func (m *encMessage) setChainLength(chainLength int) { 637 | if chainLength > maxChainLength { 638 | err := fmt.Errorf("specified chain length (%d) exceeds maximum chain length (%d)", 639 | chainLength, 640 | maxChainLength, 641 | ) 642 | panic(err) 643 | } 644 | if chainLength <= 0 { 645 | err := errors.New("chain length cannot be negative or zero") 646 | panic(err) 647 | } 648 | m.chainLength = chainLength 649 | m.intermediateHops = chainLength - 1 650 | m.padHeaders = maxChainLength - m.chainLength 651 | m.padBytes = m.padHeaders * headerBytes 652 | // The padding bytes need to be randomized, otherwise the final 653 | // intermediate remailer in the chain can know its position due to the 654 | // zero bytes below the decrypted exit header. After this, the payload 655 | // will contain nothing but padding. 656 | copy(m.payload, crandom.Randbytes(m.padBytes)) 657 | // Generate keys and (partial) IVs for each hop 658 | for n := 0; n < m.intermediateHops; n++ { 659 | m.keys[n] = crandom.Randbytes(32) 660 | m.ivs[n] = crandom.Randbytes(12) 661 | } 662 | } 663 | 664 | // setPlainText inserts the plain message content into the payload and returns 665 | // its length in Bytes 666 | func (m *encMessage) setPlainText(plain []byte) (plainLength int) { 667 | plainLength = len(plain) 668 | if m.plainLength > bodyBytes { 669 | err := fmt.Errorf("payload (%d) exceeds max length (%d)", plainLength, bodyBytes) 670 | panic(err) 671 | } 672 | // Insert the plain bytes after the headers 673 | copy(m.payload[headersBytes:], plain) 674 | m.gotPayload = true 675 | return 676 | } 677 | 678 | // getIntermediateHops returns the number of intermediate hops in the chain. 679 | func (m *encMessage) getIntermediateHops() int { 680 | if m.chainLength == 0 { 681 | err := errors.New("cannot get hop count. Chain length is not defined") 682 | panic(err) 683 | } 684 | return m.intermediateHops 685 | } 686 | 687 | // seqIV constructs a 16 Byte IV from an input of 12 random Bytes and a uint32 688 | // counter. The format is arbitrary but needs to be predictable and consistent 689 | // between encrypt and decrypt operations. 690 | func (m *encMessage) getIV(intermediateHop, slot int) (iv []byte) { 691 | if m.chainLength == 0 { 692 | err := errors.New("cannot get an IV until the chain length is defined") 693 | panic(err) 694 | } 695 | // IV format is: RRRRCCCCRRRRRRRR. Where R=Random and C=Counter 696 | iv = make([]byte, 16) 697 | copy(iv, seqIV(m.ivs[intermediateHop], slot)) 698 | return 699 | } 700 | 701 | // getKey returns the predetermined AES key for a specific Hop in the Chain. 702 | func (m *encMessage) getKey(intermediateHop int) (key []byte) { 703 | if m.chainLength == 0 { 704 | err := errors.New("cannot get a Key until the chain length is defined") 705 | panic(err) 706 | } 707 | if intermediateHop >= m.intermediateHops { 708 | err := fmt.Errorf( 709 | "requested key for hop (%d) exceeds array length (%d)", 710 | intermediateHop, 711 | m.intermediateHops, 712 | ) 713 | panic(err) 714 | } 715 | key = m.keys[intermediateHop] 716 | return 717 | } 718 | 719 | // getPartialIV returns the predetermined partial IV for a specific Hop. 720 | func (m *encMessage) getPartialIV(intermediateHop int) (ivPartial []byte) { 721 | /* 722 | It should be noted that the 12 byte partial IV returned by this 723 | function cannot be used directly to encrypt anything. It needs 724 | a 4 byte sequence number added to it in order to be usable. 725 | */ 726 | if intermediateHop > m.intermediateHops { 727 | err := fmt.Errorf( 728 | "requested IV for hop (%d) exceeds array length (%d)", 729 | intermediateHop, 730 | m.intermediateHops, 731 | ) 732 | panic(err) 733 | } 734 | ivPartial = m.ivs[intermediateHop] 735 | return 736 | } 737 | 738 | // getAntiTag returns a digest for the entire header stack. It needs to be run 739 | // before a new header is inserted but after deterministic headers are appended 740 | // to the bottom of the header stack. 741 | func (m *encMessage) getAntiTag() []byte { 742 | digest, err := blake2s.New(nil) 743 | if err != nil { 744 | panic(err) 745 | } 746 | digest.Write(m.payload[headerBytes:]) 747 | return digest.Sum(nil) 748 | } 749 | 750 | // Encrypt the body with the provided key and IV. This should only be used for 751 | // encryption of the Body during Exit-Hop encoding. At other times, encryptAll 752 | // should be used. 753 | func (m *encMessage) encryptBody(key, iv []byte) { 754 | var err error 755 | if !m.gotPayload { 756 | err = errors.New("cannot encrypt payload until it's defined") 757 | panic(err) 758 | } 759 | err = lenCheck(len(key), 32) 760 | if err != nil { 761 | panic(err) 762 | } 763 | err = lenCheck(len(iv), 16) 764 | if err != nil { 765 | panic(err) 766 | } 767 | 768 | copy( 769 | m.payload[headersBytes:], 770 | aesCtr( 771 | m.payload[headersBytes:], 772 | key, 773 | iv, 774 | ), 775 | ) 776 | } 777 | 778 | // encryptAll encrypts each Header Slot in the message using a predetermined 779 | // AES Key and partial (12 byte) IV, plus a 4 byte sequence number base on the 780 | // Slot number. Finally, the body is encrypted using the same key and partial 781 | // IV (with the next sequenced number). 782 | func (m *encMessage) encryptAll(hop int) { 783 | // The same key is used for all these encrypt operations 784 | key := m.getKey(hop) 785 | var iv []byte 786 | /* 787 | * This should run before headers are shifted down * 788 | For maxChainLength = 10:- 789 | IVs 0-8 are used to encrypt headers 790 | IV 9 is used to encrypt the payload 791 | */ 792 | for slot := 0; slot < maxChainLength; slot++ { 793 | sbyte := slot * headerBytes 794 | ebyte := (slot + 1) * headerBytes 795 | iv = m.getIV(hop, slot) 796 | copy( 797 | m.payload[sbyte:ebyte], 798 | aesCtr(m.payload[sbyte:ebyte], key, iv), 799 | ) 800 | } 801 | iv = m.getIV(hop, maxChainLength) 802 | copy( 803 | m.payload[headersBytes:], 804 | aesCtr(m.payload[headersBytes:], key, iv), 805 | ) 806 | } 807 | 808 | // shiftHeaders moves the entire header stack down by headerBytes. 809 | func (m *encMessage) shiftHeaders() { 810 | // Find a point one header size up from the bottom of the header stack 811 | bottomHeader := headersBytes - headerBytes 812 | // Move the header stack down by headerBytes 813 | copy(m.payload[headerBytes:], m.payload[:bottomHeader]) 814 | } 815 | 816 | // insertHeader copies provided header bytes into the payload 817 | func (m *encMessage) insertHeader(header []byte) { 818 | err := lenCheck(len(header), headerBytes) 819 | if err != nil { 820 | panic(err) 821 | } 822 | copy(m.payload[:headerBytes], header) 823 | } 824 | 825 | // deterministic inserts predetermined headers at the bottom of the stack. As 826 | // the stack scrolls up during decryption, a blank header is inserted at the 827 | // bottom. This is then decrypted along with all the real headers. This 828 | // hellish function works out what those headers will contain at each phase of 829 | // the remailer decryption chain. 830 | func (m *encMessage) deterministic(hop int) { 831 | if m.chainLength == 0 { 832 | err := errors.New("cannot generate deterministic headers until chain length has been specified") 833 | panic(err) 834 | } 835 | // The top and bottom slots are the slots we're populating during this 836 | // cycle. 837 | bottomSlot := maxChainLength - 1 838 | topSlot := bottomSlot - (m.intermediateHops - hop - 1) 839 | // Slot in this context is the slot the header will be placed in, on 840 | // the current hop. Not, the slot to encrypt from. 841 | for slot := topSlot; slot <= bottomSlot; slot++ { 842 | // right is the rightmost hop, from which to encrypt. 843 | right := bottomSlot - slot + hop 844 | useSlot := bottomSlot 845 | fakeHead := make([]byte, headerBytes) 846 | // Work back from the rightmost slot to the first intermediate 847 | // header. 848 | for interHop := right; interHop-hop >= 0; interHop-- { 849 | key := m.getKey(interHop) 850 | iv := m.getIV(interHop, useSlot) 851 | copy(fakeHead, aesCtr(fakeHead, key, iv)) 852 | useSlot-- 853 | } 854 | // Actually insert the fiendish header into the message 855 | sByte := slot * headerBytes 856 | eByte := sByte + headerBytes 857 | copy(m.payload[sByte:eByte], fakeHead) 858 | } 859 | } 860 | 861 | // debugPacket is only used for debugging purposes. It outputs the first 20 862 | // bytes of each message component. The last line output will be the first 20 863 | // bytes of the payload body. 864 | func (m *encMessage) debugPacket() { 865 | fmt.Println("Encrypt diagnostic") 866 | for slot := 0; slot <= maxChainLength; slot++ { 867 | sbyte := slot * headerBytes 868 | ebyte := sbyte + 20 869 | fmt.Printf( 870 | "%05d-%05d: %x %02d\n", 871 | sbyte, 872 | ebyte, 873 | m.payload[sbyte:ebyte], 874 | slot, 875 | ) 876 | } 877 | } 878 | 879 | type decMessage struct { 880 | payload []byte // The actual Yamn message 881 | } 882 | 883 | // newDecMessage creates a new decMessage object and populates it with the 884 | // provided message bytes (assumed to be an encrypted message). 885 | func newDecMessage(encPayload []byte) (dec *decMessage) { 886 | err := lenCheck(len(encPayload), messageBytes) 887 | if err != nil { 888 | panic(err) 889 | } 890 | dec = new(decMessage) 891 | dec.payload = make([]byte, messageBytes) 892 | copy(dec.payload, encPayload) 893 | return 894 | } 895 | 896 | // getHeader returns the top-most header 897 | func (m *decMessage) getHeader() []byte { 898 | return m.payload[:headerBytes] 899 | } 900 | 901 | // getPayload returns the entire payload as a byte slice 902 | func (m *decMessage) getPayload() []byte { 903 | return m.payload 904 | } 905 | 906 | // shiftHeaders moves the entire header stack up by headerBytes and chops off 907 | // the top header. The created slot of headerBytes at the bottom is 908 | // initialized. 909 | func (m *decMessage) shiftHeaders() { 910 | // Find a point one header size up from the bottom of the header stack 911 | bottomHeader := headersBytes - headerBytes 912 | // Move the header stack up by one headerBytes 913 | copy(m.payload, m.payload[headerBytes:headersBytes]) 914 | // Insert a new empty header at the bottom of the stack 915 | copy(m.payload[bottomHeader:], make([]byte, headerBytes)) 916 | } 917 | 918 | // testAntiTag creates a Blake2 hash of the entire payload (less the top 919 | // headerBytes) and compares it with the provided hash. If the two collide, it 920 | // returns True. 921 | func (m *decMessage) testAntiTag(tag []byte) bool { 922 | digest, err := blake2s.New(nil) 923 | if err != nil { 924 | panic(err) 925 | } 926 | digest.Write(m.payload[headerBytes:]) 927 | if bytes.Compare(tag, digest.Sum(nil)) == 0 { 928 | return true 929 | } 930 | return false 931 | } 932 | 933 | // Decrypt the body with the provided key and IV. This function should only be 934 | // called during exit decryption. At other times, decryptAll should be used. 935 | func (m *decMessage) decryptBody(key, iv []byte, length int) []byte { 936 | var err error 937 | err = lenCheck(len(key), 32) 938 | if err != nil { 939 | panic(err) 940 | } 941 | err = lenCheck(len(iv), 16) 942 | if err != nil { 943 | panic(err) 944 | } 945 | 946 | copy( 947 | m.payload[headersBytes:], 948 | aesCtr( 949 | m.payload[headersBytes:], 950 | key, 951 | iv, 952 | ), 953 | ) 954 | return m.payload[headersBytes : headersBytes+length] 955 | } 956 | 957 | // Decrypt each header in turn using a supplied key and partial IV. Also 958 | // decrypt the body using the same key and last IV in the sequence. 959 | func (m *decMessage) decryptAll(key, partialIV []byte) { 960 | var err error 961 | err = lenCheck(len(key), 32) 962 | if err != nil { 963 | panic(err) 964 | } 965 | err = lenCheck(len(partialIV), 12) 966 | if err != nil { 967 | panic(err) 968 | } 969 | var iv []byte 970 | for slot := 0; slot < maxChainLength; slot++ { 971 | sbyte := slot * headerBytes 972 | ebyte := (slot + 1) * headerBytes 973 | iv = seqIV(partialIV, slot) 974 | copy( 975 | m.payload[sbyte:ebyte], 976 | aesCtr(m.payload[sbyte:ebyte], key, iv), 977 | ) 978 | } 979 | // IVs from 0 to maxChainLength-1 have been used for the headers. The 980 | // next IV in sequence (maxChainLength) is used to decrypt the body. 981 | iv = seqIV(partialIV, maxChainLength) 982 | copy( 983 | m.payload[headersBytes:], 984 | aesCtr(m.payload[headersBytes:], key, iv), 985 | ) 986 | } 987 | 988 | // debugPacket is only used for debugging purposes. It outputs the first 20 989 | // bytes of each message component. The last line output will be the first 20 990 | // bytes of the payload body. 991 | func (m *decMessage) debugPacket() { 992 | fmt.Println("Decrypt diagnostic") 993 | for slot := 0; slot <= maxChainLength; slot++ { 994 | sbyte := slot * headerBytes 995 | ebyte := sbyte + 20 996 | fmt.Printf( 997 | "%05d-%05d: %x %02d\n", 998 | sbyte, 999 | ebyte, 1000 | m.payload[sbyte:ebyte], 1001 | slot, 1002 | ) 1003 | } 1004 | } 1005 | -------------------------------------------------------------------------------- /packet_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "testing" 7 | 8 | "github.com/crooks/yamn/crandom" 9 | ) 10 | 11 | func errTest(err error) { 12 | if err != nil { 13 | panic(err) 14 | } 15 | } 16 | 17 | func TestEpochTimestamp(t *testing.T) { 18 | data := newSlotData() 19 | age := data.ageTimestamp() 20 | if age < 0 || age > 3 { 21 | t.Fatalf("Epoch age should be in the range 0-3. Got: %d", age) 22 | } 23 | } 24 | 25 | func TestIntermediate(t *testing.T) { 26 | inputAesIV12 := []byte("abcdefghijkl") 27 | inputNextHop := "mrfoobar@anonymous.invalid" 28 | inInter := newSlotIntermediate() 29 | inInter.setPartialIV(inputAesIV12) 30 | inInter.setNextHop(inputNextHop) 31 | outInter := decodeIntermediate(inInter.encode()) 32 | if bytes.Compare(outInter.aesIV12, inputAesIV12) != 0 { 33 | t.Fatalf("Intermediate AES IV mismatch: %x", outInter.aesIV12) 34 | } 35 | if outInter.getNextHop() != inputNextHop { 36 | t.Fatalf( 37 | "Intermediate nextHop mismatch: %s", 38 | outInter.getNextHop(), 39 | ) 40 | } 41 | } 42 | 43 | func TestSlotData(t *testing.T) { 44 | inSlotData := newSlotData() 45 | inSlotData.setTimestamp() 46 | inSlotData.setPacketInfo(make([]byte, 64)) 47 | inSlotData.setAesKey(crandom.Randbytes(32)) 48 | inSlotData.setTagHash(make([]byte, 32)) 49 | outSlotData := decodeSlotData(inSlotData.encode()) 50 | if bytes.Compare(inSlotData.packetID, outSlotData.packetID) != 0 { 51 | t.Fatal("PacketID Mismatch") 52 | } 53 | } 54 | 55 | func TestNaClEncryptDecrypt(t *testing.T) { 56 | inHead := newEncodeHeader() 57 | inPlain := crandom.Randbytes(160) 58 | recipientPK, _ := eccGenerate() 59 | fakeKeyid := crandom.Randbytes(16) 60 | inHead.setRecipient(fakeKeyid, recipientPK) 61 | inHead.encode(inPlain) 62 | } 63 | 64 | func TestPacket(t *testing.T) { 65 | plainText := "Hello world!" 66 | 67 | outExitHead := newSlotFinal() 68 | outExitHead.setBodyBytes(len([]byte(plainText))) 69 | payload := make([]byte, bodyBytes) 70 | copy(payload, []byte(plainText)) 71 | 72 | outHead := newSlotData() 73 | outHead.setAesKey(crandom.Randbytes(32)) 74 | outHead.setTagHash(make([]byte, 32)) 75 | outHead.setPacketInfo(outExitHead.encode()) 76 | copy(payload, aesCtr(payload, outHead.aesKey, outExitHead.aesIV)) 77 | 78 | inHead := decodeSlotData(outHead.encode()) 79 | 80 | inExitHead := decodeFinal(inHead.packetInfo) 81 | if bytes.Compare(outHead.aesKey, inHead.aesKey) != 0 { 82 | t.Fatal("AES Key mismatch") 83 | } 84 | if bytes.Compare(outExitHead.aesIV, inExitHead.aesIV) != 0 { 85 | t.Fatal("AES IV mismatch") 86 | } 87 | copy(payload, aesCtr(payload, inHead.aesKey, inExitHead.aesIV)) 88 | outText := string(payload[0:inExitHead.bodyBytes]) 89 | if outText != plainText { 90 | t.Fatal("Body encrypt/decrypt mismatch") 91 | } 92 | if bytes.Compare(outExitHead.messageID, inExitHead.messageID) != 0 { 93 | t.Fatal("MessageID mismatch") 94 | } 95 | } 96 | 97 | /* 98 | func TestKeys(t *testing.T) { 99 | chainLength := 3 100 | k := newAesKeys(chainLength - 1) 101 | fmt.Printf("%x\n", k.deterministic(0)) 102 | } 103 | */ 104 | 105 | func TestOneHop(t *testing.T) { 106 | encPlain := []byte("Hello World!") 107 | exitPK, exitSK := eccGenerate() 108 | //interPK, interSK := eccGenerate() 109 | 110 | //Create Exit Header Data 111 | encSlotFinal := newSlotFinal() 112 | encSlotFinal.setBodyBytes(len(encPlain)) 113 | // Create and populate the Slot Data 114 | encSlotData := newSlotData() 115 | // Tell the Slot Data that this is the exit hop, otherwise it will 116 | // default to intermediate. 117 | encSlotData.setExit() 118 | encSlotData.setAesKey(crandom.Randbytes(32)) 119 | encSlotData.setPacketInfo(encSlotFinal.encode()) 120 | encSlotData.setTagHash(make([]byte, 32)) 121 | encSlotDataBytes := encSlotData.encode() 122 | 123 | fakeRecipientKeyID := make([]byte, 16) 124 | encHeader := newEncodeHeader() 125 | encHeader.setRecipient(fakeRecipientKeyID, exitPK) 126 | 127 | exitHeader := encHeader.encode(encSlotDataBytes) 128 | encBody := make([]byte, bodyBytes) 129 | copy(encBody, aesCtr(encPlain, encSlotData.aesKey, encSlotFinal.aesIV)) 130 | 131 | // Create a decode struct called exitHead and fill it with the encoded 132 | // bytes from encHead 133 | decHeader := newDecodeHeader(exitHeader) 134 | // We're faking the KeyID but this at least proves the function 135 | _ = decHeader.getRecipientKeyID() 136 | decHeader.setRecipientSK(exitSK) 137 | decSlotDataBytes, version, err := decHeader.decode() 138 | if err != nil { 139 | t.Fatalf("Header docode failed: %s", err) 140 | } 141 | if version != 2 { 142 | t.Fatalf( 143 | "Not a version 2 type packet. Got version: %d", 144 | version, 145 | ) 146 | } 147 | // Test if the decoded raw Slot Data bytes match the input Slot Data 148 | if bytes.Compare(encSlotDataBytes, decSlotDataBytes) != 0 { 149 | t.Fatal("Encoded/Decoded Slot Data mismatch") 150 | } 151 | // Convert the raw Slot Data Bytes to meaningful slotData. 152 | decSlotData := decodeSlotData(decSlotDataBytes) 153 | if decSlotData.packetType != 1 { 154 | t.Fatalf( 155 | "Expected Packet Type 1 (Exit Hop) but got %d", 156 | decSlotData.packetType, 157 | ) 158 | } 159 | decSlotFinal := decodeFinal(decSlotData.packetInfo) 160 | 161 | decBody := make([]byte, bodyBytes) 162 | copy(decBody, aesCtr(encBody, decSlotData.aesKey, decSlotFinal.aesIV)) 163 | decPlain := decBody[:decSlotFinal.bodyBytes] 164 | if bytes.Compare(encPlain, decPlain) != 0 { 165 | t.Fatalf( 166 | "Body decode mismatch. In=%s, Out=%s", 167 | encPlain, 168 | decPlain, 169 | ) 170 | } 171 | } 172 | func TestMultiHop(t *testing.T) { 173 | chainLength := maxChainLength 174 | m := newEncMessage() 175 | encPlain := []byte("Hello World!") 176 | plainLength := m.setPlainText(encPlain) 177 | testPK, testSK := eccGenerate() 178 | 179 | //Create Exit Header Data 180 | encFinal := newSlotFinal() 181 | encFinal.setBodyBytes(plainLength) 182 | // Create and populate the Slot Data 183 | encData := newSlotData() 184 | // Tell the Slot Data that this is the exit hop, otherwise it will 185 | // default to intermediate. 186 | encData.setExit() 187 | encData.setAesKey(crandom.Randbytes(32)) 188 | // Encode the Packet Info and store it in the Slot Data 189 | encData.setPacketInfo(encFinal.encode()) 190 | 191 | fakeRecipientKeyID := make([]byte, 16) 192 | encHeader := newEncodeHeader() 193 | encHeader.setRecipient(fakeRecipientKeyID, testPK) 194 | 195 | // Define the chain length 196 | m.setChainLength(chainLength) 197 | // Populate the message with the encrypted body 198 | m.encryptBody(encData.aesKey, encFinal.aesIV) 199 | m.shiftHeaders() 200 | if chainLength > 1 { 201 | m.deterministic(0) 202 | } 203 | //m.debugPacket() 204 | encData.setTagHash(m.getAntiTag()) 205 | // Encode the Slot Data 206 | encDataBytes := encData.encode() 207 | // Insert an byte encoded version of the newly created header 208 | m.insertHeader(encHeader.encode(encDataBytes)) 209 | //m.debugPacket() 210 | 211 | // That concludes the exit hop compilation 212 | 213 | //m.debugPacket() 214 | interHops := m.getIntermediateHops() 215 | for interHop := 0; interHop < interHops; interHop++ { 216 | encInter := newSlotIntermediate() 217 | encInter.setPartialIV(m.getPartialIV(interHop)) 218 | encInter.setNextHop("fake@remailer.org") 219 | encData = newSlotData() 220 | encData.setAesKey(m.getKey(interHop)) 221 | encData.setPacketInfo(encInter.encode()) 222 | m.encryptAll(interHop) 223 | m.shiftHeaders() 224 | m.deterministic(interHop + 1) 225 | encData.setTagHash(m.getAntiTag()) 226 | encDataBytes = encData.encode() 227 | encHeader = newEncodeHeader() 228 | encHeader.setRecipient(fakeRecipientKeyID, testPK) 229 | m.insertHeader(encHeader.encode(encDataBytes)) 230 | } 231 | 232 | // End of Intermediate hop encoding 233 | 234 | // Kludge to put the previously encrypted payload into a decMessage 235 | // struct. 236 | d := newDecMessage(m.payload) 237 | 238 | var gotExit bool 239 | for remailer := 0; remailer < maxChainLength; remailer++ { 240 | // Create a decode struct called exitHead and fill it with the 241 | // encoded bytes from encHead 242 | decHeader := newDecodeHeader(d.getHeader()) 243 | // We're faking the KeyID but this at least proves the function 244 | _ = decHeader.getRecipientKeyID() 245 | decHeader.setRecipientSK(testSK) 246 | decDataBytes, version, err := decHeader.decode() 247 | if err != nil { 248 | t.Fatalf("Header decode failed: %s", err) 249 | } 250 | if version != 2 { 251 | t.Fatalf( 252 | "Not a version 2 type packet. Got version: %d", 253 | version, 254 | ) 255 | } 256 | // Convert the raw Slot Data Bytes to meaningful slotData. 257 | decData := decodeSlotData(decDataBytes) 258 | if !d.testAntiTag(decData.getTagHash()) { 259 | d.debugPacket() 260 | fmt.Printf("Packet Type: %d\n", decData.packetType) 261 | t.Fatalf("Anti-tag fail at remailer: %d\n", remailer) 262 | } 263 | if decData.packetType == 0 { 264 | d.shiftHeaders() 265 | // Decode Intermediate 266 | decInter := decodeIntermediate(decData.packetInfo) 267 | d.decryptAll(decData.aesKey, decInter.aesIV12) 268 | } else if decData.packetType == 1 { 269 | //d.debugPacket() 270 | // Decode Exit 271 | gotExit = true 272 | decFinal := decodeFinal(decData.packetInfo) 273 | 274 | decPlain := d.decryptBody( 275 | decData.aesKey, 276 | decFinal.aesIV, 277 | decFinal.bodyBytes, 278 | ) 279 | if bytes.Compare(encPlain, decPlain) != 0 { 280 | t.Fatalf( 281 | "Body decode mismatch. In=%s, Out=%s", 282 | encPlain, 283 | decPlain, 284 | ) 285 | } 286 | } else { 287 | t.Fatalf("Unknown Packet Type: %d", decData.packetInfo) 288 | } 289 | 290 | if gotExit { 291 | break 292 | } 293 | } 294 | if !gotExit { 295 | t.Fatal("Decode loop ended without finding an exit header") 296 | } 297 | } 298 | -------------------------------------------------------------------------------- /pool.go: -------------------------------------------------------------------------------- 1 | // vim: tabstop=2 shiftwidth=2 2 | 3 | package main 4 | 5 | import ( 6 | "encoding/hex" 7 | "errors" 8 | "io/ioutil" 9 | "net/mail" 10 | "os" 11 | "path" 12 | "strings" 13 | "time" 14 | 15 | //"github.com/codahale/blake2" 16 | "github.com/Masterminds/log-go" 17 | "github.com/crooks/yamn/crandom" 18 | "github.com/crooks/yamn/keymgr" 19 | "github.com/luksen/maildir" 20 | ) 21 | 22 | // serverPoolOutboundSend is intended to be run concurrently with the server 23 | // daemon. It sends messages from the pool at timed intervals. 24 | func serverPoolOutboundSend() { 25 | if cfg.Pool.Loop < 120 { 26 | log.Warnf( 27 | "Pool loop of %d Seconds is too short. "+ 28 | "Adjusting to minimum of 120 Seconds.", 29 | cfg.Pool.Loop, 30 | ) 31 | cfg.Pool.Loop = 120 32 | } 33 | sleepFor := time.Duration(cfg.Pool.Loop) * time.Second 34 | for { 35 | // Read dynamic mix of outbound files from the Pool 36 | // filenames = dynamicMix() 37 | // Read binomialMix of outbound files from the Pool 38 | filenames := binomialMix() 39 | for _, filename := range filenames { 40 | emailPoolFile(filename) 41 | } 42 | time.Sleep(sleepFor) 43 | } 44 | } 45 | 46 | // poolOutboundSend flushes the outbound pool. This should only be performed 47 | // on clients, where all messages should be sent instantly after creation. 48 | func poolOutboundSend() { 49 | var err error 50 | if cfg.Remailer.Daemon || flag.Daemon { 51 | // This should never happen. If the server is started as a 52 | // daemon, the serverPoolOutboundSend process is initiated and 53 | // runs in an endless loop. This function would conflict with 54 | // it. 55 | err = errors.New("cannot flush pool when running as a daemon") 56 | panic(err) 57 | } 58 | var filenames []string 59 | // Read all the pool files 60 | filenames, err = readDir(cfg.Files.Pooldir, "m") 61 | if err != nil { 62 | log.Warnf("Reading pool failed: %s", err) 63 | return 64 | } 65 | if flag.Remailer { 66 | // During normal operation, the pool shouldn't be flushed. 67 | log.Warn("Flushing outbound remailer pool") 68 | } 69 | for _, filename := range filenames { 70 | emailPoolFile(filename) 71 | } 72 | } 73 | 74 | // emailPoolFile tries to email a given file from the Pool. If conditions are 75 | // met, the file is then deleted. 76 | func emailPoolFile(filename string) { 77 | delFlag, err := mailPoolFile(path.Join(cfg.Files.Pooldir, filename)) 78 | if err != nil { 79 | log.Warnf("Pool mailing failed: %s", err) 80 | if delFlag { 81 | // If delFlag is true, we delete the file, even though 82 | // mailing failed. 83 | poolDelete(filename) 84 | } 85 | } else { 86 | stats.outMail++ 87 | poolDelete(filename) 88 | } 89 | } 90 | 91 | // dynamicMix returns a dynamic Mix of filenames from the outbound pool. 92 | func dynamicMix() []string { 93 | var empty []string 94 | poolFiles, err := readDir(cfg.Files.Pooldir, "m") 95 | if err != nil { 96 | log.Warnf("Unable to access pool: %s", err) 97 | return empty 98 | } 99 | poolSize := len(poolFiles) 100 | if poolSize < cfg.Pool.Size || poolSize == 0 { 101 | // Pool isn't sufficiently populated 102 | log.Tracef( 103 | "Pool insufficiently populated to trigger sending."+ 104 | "Require=%d, Got=%d", 105 | cfg.Pool.Size, 106 | poolSize, 107 | ) 108 | return empty 109 | } 110 | // Shuffle the slice of filenames now as we're going to return a 111 | // setset of the overall pool. 112 | crandom.Shuffle(poolFiles) 113 | // Normal pool processing condition 114 | numToSend := int((float32(poolSize) / 100.0) * float32(cfg.Pool.Rate)) 115 | log.Tracef("Processing %d pool messages.\n", poolSize) 116 | return poolFiles[:numToSend] 117 | } 118 | 119 | // getBatchSize takes a Pool size and returns a corresponding batch size. This 120 | // is intended for use with Binomial Mix Pools. 121 | func getBatchSize(poolSize int) int { 122 | /* 123 | poolSize - Number of files in the pool 124 | cfg.Pool.Size - Minimum messages to keep in pool 125 | cfg.Pool.MinSend - Minimum number of messages to consider sending 126 | cfg.Pool.Rate - Percentage of Pool in the batch 127 | */ 128 | if poolSize < (cfg.Pool.Size + cfg.Pool.MinSend) { 129 | return 0 130 | } 131 | sendable := poolSize - cfg.Pool.Size 132 | rate := float32(cfg.Pool.Rate) / 100 133 | maxSend := max(1, int(float32(poolSize)*rate)) 134 | return min(sendable, maxSend) 135 | } 136 | 137 | // binomialMix returns a batched subset of Pool files to send using a 138 | // Probability B/P method of selecting each file. 139 | func binomialMix() (batch []string) { 140 | poolFiles, err := readDir(cfg.Files.Pooldir, "m") 141 | if err != nil { 142 | log.Warnf("Unable to access pool: %s", err) 143 | return 144 | } 145 | poolSize := len(poolFiles) 146 | batchSize := getBatchSize(poolSize) 147 | if batchSize == 0 { 148 | log.Tracef("Binomial Mix Pool: Size=%d", poolSize) 149 | // If the batch is empty, don't bother to process it. 150 | return 151 | } 152 | // Shuffle the slice of filenames now as we're only going to consider a 153 | // subset in the following loop. 154 | crandom.Shuffle(poolFiles) 155 | // Multiply probability by 255 as dice() returns 0-255. 156 | prob := int((float32(batchSize) / float32(poolSize)) * 255) 157 | // Test each pool filename against a biased coin-toss 158 | for _, s := range poolFiles[:batchSize] { 159 | if prob >= crandom.Dice() { 160 | batch = append(batch, s) 161 | } 162 | } 163 | log.Tracef( 164 | "Binomial Mix Pool: Size=%d, Batch=%d, Prob=%d/255, Sending=%d", 165 | poolSize, 166 | batchSize, 167 | prob, 168 | len(batch), 169 | ) 170 | return 171 | } 172 | 173 | // Delete a given file from the pool 174 | func poolDelete(filename string) { 175 | // Delete a pool file 176 | err := os.Remove(path.Join(cfg.Files.Pooldir, filename)) 177 | if err != nil { 178 | log.Errorf("Failed to remove %s from %s\n", filename, cfg.Files.Pooldir) 179 | } else { 180 | log.Tracef("Deleted %s from Pool", filename) 181 | } 182 | } 183 | 184 | // processMail reads the Remailer's Maildir and processes the content 185 | func processMail(secret *keymgr.Secring) (err error) { 186 | dir := maildir.Dir(cfg.Files.Maildir) 187 | // Get a list of Maildir keys from the directory 188 | keys, err := dir.Unseen() 189 | if err != nil { 190 | return 191 | } 192 | newMsgs := len(keys) 193 | if newMsgs == 0 { 194 | // Nothing to do, move along! 195 | return 196 | } 197 | log.Tracef( 198 | "Reading %d messages from %s\n", 199 | newMsgs, 200 | cfg.Files.Maildir, 201 | ) 202 | // Increment inbound Email counter 203 | stats.inMail += newMsgs 204 | // Fetch headers for each Maildir key 205 | var head mail.Header 206 | for _, key := range keys { 207 | head, err = dir.Header(key) 208 | if err != nil { 209 | log.Warnf("%s: Getting headers failed with: %s", key, err) 210 | continue 211 | } 212 | // The Subject determines if the message needs remailer-foo handling 213 | subject := strings.TrimSpace(strings.ToLower(head.Get("Subject"))) 214 | if strings.HasPrefix(subject, "remailer-") { 215 | // It's a remailer-foo request 216 | err = remailerFoo(subject, head.Get("From")) 217 | if err == nil { 218 | // Increments stats counter 219 | stats.inRemFoo++ 220 | } else { 221 | log.Info(err) 222 | } 223 | err = dir.Purge(key) 224 | if err != nil { 225 | log.Warnf( 226 | "Cannot delete remailer-foo mail: %s", 227 | err, 228 | ) 229 | } 230 | // Nothing else to do, move on to the next message 231 | continue 232 | } 233 | // It's not a remailer-foo request so assume a remailer message 234 | var mailMsg *mail.Message 235 | mailMsg, err := dir.Message(key) 236 | if err != nil { 237 | log.Warnf( 238 | "%s: Reading message failed with: %s", 239 | key, 240 | err, 241 | ) 242 | continue 243 | } 244 | var msg []byte 245 | // Convert the armored Yamn message to its byte components 246 | msg, err = stripArmor(mailMsg.Body) 247 | if err != nil { 248 | log.Info(err) 249 | continue 250 | } 251 | if msg == nil { 252 | log.Warn("Dearmor returned zero bytes") 253 | continue 254 | } 255 | err = decodeMsg(msg, secret) 256 | if err != nil { 257 | log.Info(err) 258 | } 259 | err = dir.Purge(key) 260 | if err != nil { 261 | log.Warnf("Cannot delete mail: %s", err) 262 | } 263 | } // Maildir keys loop 264 | return 265 | } 266 | 267 | // processInpool is similar to processMail but reads the Inbound Pool 268 | func processInpool(prefix string, secret *keymgr.Secring) { 269 | poolFiles, err := readDir(cfg.Files.Pooldir, prefix) 270 | if err != nil { 271 | log.Warnf("Unable to access inbound pool: %s", err) 272 | return 273 | } 274 | poolSize := len(poolFiles) 275 | processed := 0 276 | for _, f := range poolFiles { 277 | filename := path.Join(cfg.Files.Pooldir, f) 278 | msg := make([]byte, messageBytes) 279 | msg, err = ioutil.ReadFile(filename) 280 | if err != nil { 281 | log.Warnf("Failed to read %s from pool: %s", f, err) 282 | continue 283 | } 284 | err = decodeMsg(msg, secret) 285 | if err != nil { 286 | log.Warn(err) 287 | } 288 | poolDelete(f) 289 | processed++ 290 | } 291 | if poolSize > 0 { 292 | log.Tracef("Inbound pool processing complete. Read=%d, Decoded=%d", 293 | poolSize, processed) 294 | } 295 | } 296 | 297 | // randPoolFilename returns a random filename with a given prefix. This should 298 | // be used in all instances where a new pool file is required. 299 | func randPoolFilename(prefix string) (fqfn string) { 300 | for { 301 | outfileName := prefix + hex.EncodeToString(crandom.Randbytes(7)) 302 | fqfn = path.Join(cfg.Files.Pooldir, outfileName) 303 | _, err := os.Stat(fqfn) 304 | if err != nil { 305 | // For once we want an error (indicating the file 306 | // doesn't exist) 307 | break 308 | } 309 | } 310 | return 311 | } 312 | 313 | // newPoolFile opens a new file in Write mode and sets user-only permissions 314 | func newPoolFile(prefix string) (f *os.File, err error) { 315 | /* 316 | Currently supported prefixs are:- 317 | [ m Oubound message (final or intermediate) ] 318 | [ i Inbound message (destined for this remailer ] 319 | [ p Partial message chunk needing assembly ] 320 | */ 321 | fqfn := randPoolFilename(prefix) 322 | f, err = os.OpenFile(fqfn, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0600) 323 | return 324 | } 325 | 326 | // writeMessageToPool requires a recipient address (another remailer) and a 327 | // payload (that gets Base64 armored). 328 | func writeMessageToPool(sendTo string, payload []byte) { 329 | f, err := newPoolFile("m") 330 | if err != nil { 331 | panic(err) 332 | } 333 | defer f.Close() 334 | // Add mail headers to the pool file 335 | writeInternalHeader(f) 336 | writeMailHeaders(f, sendTo) 337 | // Armor the payload 338 | armor(f, payload) 339 | } 340 | 341 | // writePlainToPool writes a plaintext file to the pool and returns the filename 342 | func writePlainToPool(payload []byte, prefix string) (filename string) { 343 | f, err := newPoolFile(prefix) 344 | if err != nil { 345 | panic(err) 346 | } 347 | defer f.Close() 348 | writeInternalHeader(f) 349 | f.Write(payload) 350 | _, filename = path.Split(f.Name()) 351 | return 352 | } 353 | -------------------------------------------------------------------------------- /quickmail/quickmail.go: -------------------------------------------------------------------------------- 1 | // vim: tabstop=2 shiftwidth=2 2 | 3 | package quickmail 4 | 5 | import ( 6 | "bytes" 7 | "errors" 8 | "fmt" 9 | "os" 10 | ) 11 | 12 | type message struct { 13 | headers map[string]string 14 | Prefix string 15 | Filename string 16 | Items []string 17 | Suffix string 18 | } 19 | 20 | func NewMessage() *message { 21 | return &message{headers: make(map[string]string)} 22 | } 23 | 24 | func (m message) Set(head, content string) { 25 | m.headers[head] = content 26 | } 27 | 28 | func (m message) Get(head string) string { 29 | return m.headers[head] 30 | } 31 | 32 | func (m message) Del(head string) { 33 | delete(m.headers, head) 34 | } 35 | 36 | func (m *message) Text(t string) { 37 | m.Prefix += t 38 | } 39 | 40 | func (m *message) List(l []string) { 41 | m.Items = l 42 | } 43 | 44 | func (m message) Compile() (b []byte, err error) { 45 | var ok bool 46 | _, ok = m.headers["From"] 47 | if !ok { 48 | err = errors.New("Compulsory From header not defined") 49 | return 50 | } 51 | _, ok = m.headers["To"] 52 | if !ok { 53 | err = errors.New("Compulsory To header not defined") 54 | return 55 | } 56 | buf := new(bytes.Buffer) 57 | for h := range m.headers { 58 | buf.WriteString(fmt.Sprintf("%s: %s\n", h, m.headers[h])) 59 | } 60 | buf.WriteString("\n") 61 | if m.Prefix != "" { 62 | buf.WriteString(m.Prefix) 63 | buf.WriteString("\n") 64 | } 65 | if m.Filename != "" { 66 | var f *os.File 67 | f, err = os.Open(m.Filename) 68 | if err != nil { 69 | return 70 | } 71 | defer f.Close() 72 | _, err = buf.ReadFrom(f) 73 | buf.WriteString("\n") 74 | } 75 | if len(m.Items) > 0 { 76 | for _, entry := range m.Items { 77 | buf.WriteString(entry + "\n") 78 | } 79 | } 80 | if m.Suffix != "" { 81 | buf.WriteString(m.Suffix) 82 | buf.WriteString("\n") 83 | } 84 | b = buf.Bytes() 85 | return 86 | } 87 | -------------------------------------------------------------------------------- /server.go: -------------------------------------------------------------------------------- 1 | // vim: tabstop=2 shiftwidth=2 2 | 3 | package main 4 | 5 | import ( 6 | "errors" 7 | "fmt" 8 | "io/ioutil" 9 | "os" 10 | "path" 11 | "strings" 12 | "time" 13 | 14 | "github.com/Masterminds/log-go" 15 | "github.com/crooks/yamn/crandom" 16 | "github.com/crooks/yamn/idlog" 17 | "github.com/crooks/yamn/keymgr" 18 | "github.com/crooks/yamn/quickmail" 19 | //"github.com/codahale/blake2" 20 | ) 21 | 22 | // Start the server process. If run with --daemon, this will loop forever. 23 | func loopServer() (err error) { 24 | // Initialize the Public Keyring 25 | Pubring = keymgr.NewPubring( 26 | cfg.Files.Pubring, 27 | cfg.Files.Mlist2, 28 | ) 29 | // Fetch keyring and stats URLs 30 | timedURLFetch(cfg.Urls.Pubring, cfg.Files.Pubring) 31 | timedURLFetch(cfg.Urls.Mlist2, cfg.Files.Mlist2) 32 | // Initialize the Secret Keyring 33 | secret := keymgr.NewSecring(cfg.Files.Secring, cfg.Files.Pubkey) 34 | Pubring.ImportPubring() 35 | secret.ImportSecring() 36 | // Tell the secret keyring some basic info about this remailer 37 | secret.SetName(cfg.Remailer.Name) 38 | secret.SetAddress(cfg.Remailer.Address) 39 | secret.SetExit(cfg.Remailer.Exit) 40 | secret.SetValidity(cfg.Remailer.Keylife, cfg.Remailer.Keygrace) 41 | secret.SetVersion(version) 42 | // Create some dirs if they don't already exist 43 | createDirs() 44 | 45 | // Open the IDlog 46 | log.Tracef("Opening ID Log: %s", cfg.Files.IDlog) 47 | // NewInstance takes the filename and entry validity in days 48 | IDDb = idlog.NewIDLog(cfg.Files.IDlog, cfg.Remailer.IDexp) 49 | defer IDDb.Close() 50 | // Open the chunk DB 51 | log.Tracef("Opening the Chunk DB: %s", cfg.Files.ChunkDB) 52 | ChunkDb = OpenChunk(cfg.Files.ChunkDB) 53 | ChunkDb.SetExpire(cfg.Remailer.ChunkExpire) 54 | 55 | // Expire old entries in the ID Log 56 | idLogExpire() 57 | // Clean the chunk DB 58 | chunkClean() 59 | // Complain about poor configs 60 | nagOperator() 61 | // Run a key purge 62 | if purgeSecring(secret) == 0 { 63 | // If there are zero active keys, generate a new one. 64 | generateKeypair(secret) 65 | } else { 66 | /* 67 | If the operator changes his configuration, (such as 68 | upgrading to a new version or switching from exit to 69 | middleman), the published key will not match the 70 | configuration. This element of code writes a new 71 | key.txt file with current settings. This only needs to 72 | be done if we haven't generated a new key. 73 | */ 74 | refreshPubkey(secret) 75 | } 76 | 77 | log.Infof("Secret keyring contains %d keys", secret.Count()) 78 | 79 | // Define triggers for timed events 80 | daily := time.Now() 81 | hourly := time.Now() 82 | dayOfMonth := time.Now().Day() 83 | oneDay := time.Duration(dayLength) * time.Second 84 | 85 | // Determine if this is a single run or the start of a Daemon 86 | runAsDaemon := cfg.Remailer.Daemon || flag.Daemon 87 | 88 | // Actually start the server loop 89 | if runAsDaemon { 90 | log.Infof("Starting YAMN server: %s", cfg.Remailer.Name) 91 | log.Infof("Detaching Pool processing") 92 | go serverPoolOutboundSend() 93 | } else { 94 | log.Infof("Performing routine remailer functions for: %s", 95 | cfg.Remailer.Name) 96 | } 97 | for { 98 | // Panic if the pooldir doesn't exist 99 | assertIsPath(cfg.Files.Pooldir) 100 | // Process the inbound Pool 101 | processInpool("i", secret) 102 | // Process the Maildir 103 | processMail(secret) 104 | 105 | // Midnight events 106 | if time.Now().Day() != dayOfMonth { 107 | log.Info("Performing midnight events") 108 | // Remove expired keys from memory and rewrite a 109 | // secring file without expired keys. 110 | if purgeSecring(secret) == 0 { 111 | generateKeypair(secret) 112 | } 113 | // Expire entries in the ID Log 114 | idLogExpire() 115 | // Expire entries in the chunker 116 | chunkClean() 117 | // Report daily throughput and reset to zeros 118 | stats.report() 119 | stats.reset() 120 | // Reset dayOfMonth to today 121 | dayOfMonth = time.Now().Day() 122 | } 123 | // Daily events 124 | if time.Since(daily) > oneDay { 125 | log.Info("Performing daily events") 126 | // Complain about poor configs 127 | nagOperator() 128 | // Reset today so we don't do these tasks for the next 129 | // 24 hours. 130 | daily = time.Now() 131 | } 132 | // Hourly events 133 | if time.Since(hourly) > time.Hour { 134 | log.Trace("Performing hourly events") 135 | /* 136 | The following two conditions try to import new 137 | pubring and mlist2 URLs. If they fail, a 138 | warning is logged but no further action is 139 | taken. It's better to have old keys/stats than 140 | none. 141 | */ 142 | // Retrieve Mlist2 and Pubring URLs 143 | if cfg.Urls.Fetch { 144 | timedURLFetch( 145 | cfg.Urls.Pubring, 146 | cfg.Files.Pubring, 147 | ) 148 | timedURLFetch( 149 | cfg.Urls.Mlist2, 150 | cfg.Files.Mlist2, 151 | ) 152 | } 153 | // Test to see if the pubring.mix file has been updated 154 | if Pubring.KeyRefresh() { 155 | log.Tracef( 156 | "Reimporting Public Keyring: %s", 157 | cfg.Files.Pubring, 158 | ) 159 | Pubring.ImportPubring() 160 | } 161 | // Report throughput 162 | stats.report() 163 | hourly = time.Now() 164 | } 165 | 166 | // Break out of the loop if we're not running as a daemon 167 | if !runAsDaemon { 168 | break 169 | } 170 | 171 | // And rest a while 172 | time.Sleep(60 * time.Second) 173 | } // End of server loop 174 | return 175 | } 176 | 177 | // refreshPubkey updates an existing Public key file 178 | func refreshPubkey(secret *keymgr.Secring) { 179 | tmpKey := cfg.Files.Pubkey + ".tmp" 180 | keyidstr := secret.WriteMyKey(tmpKey) 181 | log.Infof("Advertising keyid: %s", keyidstr) 182 | log.Tracef("Writing current public key to %s", tmpKey) 183 | // Overwrite the published key with the refreshed version 184 | log.Tracef("Renaming %s to %s", tmpKey, cfg.Files.Pubkey) 185 | err := os.Rename(tmpKey, cfg.Files.Pubkey) 186 | if err != nil { 187 | log.Warn(err) 188 | } 189 | } 190 | 191 | // purgeSecring deletes old keys and counts active ones. If no active keys 192 | // are found, it triggers a generation. 193 | func purgeSecring(secret *keymgr.Secring) (active int) { 194 | active, expiring, expired, purged := secret.Purge() 195 | log.Infof( 196 | "Key purge complete. Active=%d, Expiring=%d, Expired=%d, "+ 197 | "Purged=%d", 198 | active, 199 | expiring, 200 | expired, 201 | purged, 202 | ) 203 | return 204 | } 205 | 206 | // generateKeypair creates a new keypair and publishes it 207 | func generateKeypair(secret *keymgr.Secring) { 208 | log.Info("Generating and advertising a new key pair") 209 | pub, sec := eccGenerate() 210 | keyidstr := secret.Insert(pub, sec) 211 | log.Infof("Generated new keypair with keyid: %s", keyidstr) 212 | log.Info("Writing new Public Key to disc") 213 | secret.WritePublic(pub, keyidstr) 214 | log.Info("Inserting Secret Key into Secring") 215 | secret.WriteSecret(keyidstr) 216 | } 217 | 218 | // idLogExpire deletes old entries in the ID Log 219 | func idLogExpire() { 220 | count, deleted := IDDb.Expire() 221 | log.Infof("ID Log: Expired=%d, Contains=%d", deleted, count) 222 | } 223 | 224 | // chunkClean expires entries from the chunk DB and deletes any stranded files 225 | func chunkClean() { 226 | cret, cexp := ChunkDb.Expire() 227 | if cexp > 0 { 228 | log.Infof( 229 | "Chunk expiry complete. Retained=%d, Expired=%d\n", 230 | cret, 231 | cexp, 232 | ) 233 | } 234 | fret, fdel := ChunkDb.Housekeep() 235 | if fdel > 0 { 236 | log.Infof( 237 | "Stranded chunk deletion: Retained=%d, Deleted=%d", 238 | fret, 239 | fdel, 240 | ) 241 | } 242 | } 243 | 244 | // nagOperator prompts a remailer operator about poor practices. 245 | func nagOperator() { 246 | // Complain about excessively small loop values. 247 | if cfg.Pool.Loop < 60 { 248 | log.Warnf( 249 | "Loop time of %d is excessively low. Will loop "+ 250 | "every 60 seconds. A higher setting is recommended.", 251 | cfg.Pool.Loop, 252 | ) 253 | } 254 | // Complain about high pool rates. 255 | if cfg.Pool.Rate > 90 && !flag.Send { 256 | log.Warnf( 257 | "Your pool rate of %d is excessively high. Unless "+ 258 | "testing, a lower setting is recommended.", 259 | cfg.Pool.Rate, 260 | ) 261 | } 262 | // Complain about running a remailer with flag_send 263 | if flag.Send && flag.Remailer { 264 | log.Warnf( 265 | "Your remailer will flush the outbound pool every "+ 266 | "%d seconds. Unless you're testing, this is "+ 267 | "probably not what you want.", 268 | cfg.Pool.Loop, 269 | ) 270 | } 271 | } 272 | 273 | func createDirs() { 274 | var err error 275 | err = os.MkdirAll(cfg.Files.IDlog, 0700) 276 | if err != nil { 277 | log.Errorf( 278 | "Failed to create %s. %s", 279 | cfg.Files.IDlog, 280 | err, 281 | ) 282 | os.Exit(1) 283 | } 284 | err = os.MkdirAll(cfg.Files.Pooldir, 0700) 285 | if err != nil { 286 | log.Errorf( 287 | "Failed to create %s. %s", 288 | cfg.Files.Pooldir, 289 | err, 290 | ) 291 | os.Exit(1) 292 | } 293 | err = os.MkdirAll(cfg.Files.ChunkDB, 0700) 294 | if err != nil { 295 | log.Errorf( 296 | "Failed to create %s. %s", 297 | cfg.Files.ChunkDB, 298 | err, 299 | ) 300 | os.Exit(1) 301 | } 302 | err = os.MkdirAll(cfg.Files.Maildir, 0700) 303 | if err != nil { 304 | log.Errorf( 305 | "Failed to create %s. %s", 306 | cfg.Files.Maildir, 307 | err, 308 | ) 309 | os.Exit(1) 310 | } 311 | mdirnew := path.Join(cfg.Files.Maildir, "new") 312 | err = os.MkdirAll(mdirnew, 0700) 313 | if err != nil { 314 | log.Errorf("Failed to create %s. %s", mdirnew, err) 315 | os.Exit(1) 316 | } 317 | mdircur := path.Join(cfg.Files.Maildir, "cur") 318 | err = os.MkdirAll(mdircur, 0700) 319 | if err != nil { 320 | log.Errorf("Failed to create %s. %s", mdircur, err) 321 | os.Exit(1) 322 | } 323 | mdirtmp := path.Join(cfg.Files.Maildir, "tmp") 324 | err = os.MkdirAll(mdirtmp, 0700) 325 | if err != nil { 326 | log.Errorf("Failed to create %s. %s", mdirtmp, err) 327 | os.Exit(1) 328 | } 329 | } 330 | 331 | // decodeMsg is the actual YAMN message decoder. It's output is always a 332 | // pooled file, either in the Inbound or Outbound queue. 333 | func decodeMsg(rawMsg []byte, secret *keymgr.Secring) (err error) { 334 | 335 | // At this point, rawMsg should always be messageBytes in length 336 | err = lenCheck(len(rawMsg), messageBytes) 337 | if err != nil { 338 | log.Error(err) 339 | return 340 | } 341 | 342 | d := newDecMessage(rawMsg) 343 | // Extract the top header 344 | header := newDecodeHeader(d.getHeader()) 345 | recipientKeyID := header.getRecipientKeyID() 346 | recipientSK, err := secret.GetSK(recipientKeyID) 347 | if err != nil { 348 | log.Warnf("Failed to ascertain Recipient SK: %s", err) 349 | return 350 | } 351 | header.setRecipientSK(recipientSK) 352 | 353 | slotDataBytes, packetVersion, err := header.decode() 354 | if err != nil { 355 | log.Warnf("Header decode failed: %s", err) 356 | return 357 | } 358 | switch packetVersion { 359 | case 2: 360 | err = decodeV2(d, slotDataBytes) 361 | default: 362 | err = fmt.Errorf("cannot decode packet version %d", packetVersion) 363 | } 364 | return 365 | } 366 | 367 | func decodeV2(d *decMessage, slotDataBytes []byte) (err error) { 368 | // Convert the raw Slot Data Bytes to meaningful slotData. 369 | slotData := decodeSlotData(slotDataBytes) 370 | // Test uniqueness of packet ID 371 | if !IDDb.Unique(slotData.getPacketID()) { 372 | err = errors.New("packet ID collision") 373 | return 374 | } 375 | if !d.testAntiTag(slotData.getTagHash()) { 376 | log.Warn("Anti-tag digest mismatch") 377 | return 378 | } 379 | if slotData.ageTimestamp() > cfg.Remailer.MaxAge { 380 | log.Warnf( 381 | "Max packet age in days exceeded. Age=%d, Max=%d", 382 | slotData.ageTimestamp(), 383 | cfg.Remailer.MaxAge, 384 | ) 385 | return 386 | } 387 | if slotData.ageTimestamp() < 0 { 388 | log.Warn("Packet timestamp is in the future. Rejecting") 389 | return 390 | } 391 | if slotData.getPacketType() == 0 { 392 | d.shiftHeaders() 393 | // Decode Intermediate 394 | inter := decodeIntermediate(slotData.packetInfo) 395 | d.decryptAll(slotData.aesKey, inter.aesIV12) 396 | /* 397 | The following conditional tests if we are the next hop 398 | in addition to being the current hop. If we are, then 399 | it's better to store the message in the inbound pool. 400 | This prevents it being emailed back to us. 401 | */ 402 | if inter.getNextHop() == cfg.Remailer.Address { 403 | log.Info( 404 | "Message loops back to us.", 405 | "Storing in pool instead of sending it.") 406 | outfileName := randPoolFilename("i") 407 | err = ioutil.WriteFile( 408 | outfileName, 409 | d.getPayload(), 410 | 0600, 411 | ) 412 | if err != nil { 413 | log.Warnf("Failed to write to pool: %s", err) 414 | return 415 | } 416 | stats.outLoop++ 417 | } else { 418 | writeMessageToPool(inter.getNextHop(), d.getPayload()) 419 | stats.outYamn++ 420 | // Decide if we want to inject a dummy 421 | if !flag.NoDummy && crandom.Dice() < 55 { 422 | dummy() 423 | stats.outDummy++ 424 | } 425 | } // End of local or remote delivery 426 | } else if slotData.getPacketType() == 1 { 427 | // Decode Exit 428 | final := decodeFinal(slotData.packetInfo) 429 | if final.getDeliveryMethod() == 255 { 430 | log.Trace("Discarding dummy message") 431 | stats.inDummy++ 432 | return 433 | } 434 | // Decrypt the payload body 435 | // This could be done under Delivery Method 0 but, future 436 | // delivery methods (other than dummies) will require a 437 | // decrypted body. 438 | plain := d.decryptBody( 439 | slotData.getAesKey(), 440 | final.getAesIV(), 441 | final.getBodyBytes(), 442 | ) 443 | // Test delivery methods 444 | switch final.getDeliveryMethod() { 445 | case 0: 446 | stats.inYamn++ 447 | if !cfg.Remailer.Exit { 448 | if final.numChunks == 1 { 449 | // Need to randhop as we're not an exit 450 | // remailer 451 | randhop(plain) 452 | } else { 453 | log.Warn( 454 | "Randhopping doesn't support " + 455 | "multi-chunk messages. As " + 456 | "per Mixmaster, this " + 457 | "message will be dropped.", 458 | ) 459 | } 460 | return 461 | } 462 | smtpMethod(plain, final) 463 | default: 464 | log.Warnf( 465 | "Unsupported Delivery Method: %d", 466 | final.getDeliveryMethod(), 467 | ) 468 | return 469 | } 470 | } else { 471 | log.Warnf( 472 | "Unknown Packet Type: %d", 473 | slotData.getPacketType(), 474 | ) 475 | return 476 | } 477 | return 478 | } 479 | 480 | // smtpMethod is concerned with final-hop processing. 481 | func smtpMethod(plain []byte, final *slotFinal) { 482 | var err error 483 | if final.getNumChunks() == 1 { 484 | // If this is a single chunk message, pool it and get out. 485 | writePlainToPool(plain, "m") 486 | stats.outPlain++ 487 | return 488 | } 489 | // We're an exit and this is a multi-chunk message 490 | chunkFilename := writePlainToPool(plain, "p") 491 | log.Tracef( 492 | "Pooled partial chunk. MsgID=%x, Num=%d, "+ 493 | "Parts=%d, Filename=%s", 494 | final.getMessageID(), 495 | final.getChunkNum(), 496 | final.getNumChunks(), 497 | chunkFilename, 498 | ) 499 | // Fetch the chunks info from the DB for the given message ID 500 | chunks := ChunkDb.Get(final.getMessageID(), final.getNumChunks()) 501 | // This saves losts of -1's as slices start at 0 and chunks at 1 502 | cslot := final.getChunkNum() - 1 503 | // Test that the slot for this chunk is empty 504 | if chunks[cslot] != "" { 505 | log.Warnf( 506 | "Duplicate chunk %d in MsgID: %x", 507 | final.chunkNum, 508 | final.messageID, 509 | ) 510 | } 511 | // Insert the new chunk into the slice 512 | chunks[cslot] = chunkFilename 513 | log.Tracef( 514 | "Chunk state: %s", 515 | strings.Join(chunks, ","), 516 | ) 517 | // Test if all chunk slots are populated 518 | if IsPopulated(chunks) { 519 | newPoolFile := randPoolFilename("m") 520 | log.Tracef( 521 | "Assembling chunked message into %s", 522 | newPoolFile, 523 | ) 524 | err = ChunkDb.Assemble(newPoolFile, chunks) 525 | if err != nil { 526 | log.Warnf("Chunk assembly failed: %s", err) 527 | // Don't return here or the bad chunk will remain in 528 | // the DB. 529 | } 530 | // Now the message is assembled into the Pool, the DB record 531 | // can be deleted 532 | ChunkDb.Delete(final.getMessageID()) 533 | stats.outPlain++ 534 | } else { 535 | // Write the updated chunk status to 536 | // the DB 537 | ChunkDb.Insert(final.getMessageID(), chunks) 538 | } 539 | } 540 | 541 | // randhop is a simplified client function that does single-hop encodings 542 | func randhop(plainMsg []byte) { 543 | var err error 544 | if len(plainMsg) == 0 { 545 | log.Info("Zero-byte message during randhop, ignoring it.") 546 | return 547 | } 548 | // Make a single hop chain with a random node 549 | inChain := []string{"*"} 550 | final := newSlotFinal() 551 | var chain []string 552 | chain, err = makeChain(inChain) 553 | if err != nil { 554 | log.Warn(err) 555 | return 556 | } 557 | sendTo := chain[0] 558 | if len(chain) != 1 { 559 | err = fmt.Errorf("randhop chain must be single hop. Got=%d", len(chain)) 560 | panic(err) 561 | } 562 | log.Tracef("Performing a random hop to Exit Remailer: %s.", chain[0]) 563 | yamnMsg := encodeMsg(plainMsg, chain, *final) 564 | writeMessageToPool(sendTo, yamnMsg) 565 | stats.outRandhop++ 566 | } 567 | 568 | // remailerFoo responds to requests for remailer-* info 569 | func remailerFoo(subject, sender string) (err error) { 570 | m := quickmail.NewMessage() 571 | m.Set("From", cfg.Remailer.Address) 572 | m.Set("To", sender) 573 | if strings.HasPrefix(subject, "remailer-key") { 574 | // remailer-key 575 | log.Tracef("remailer-key request from %s", sender) 576 | m.Set("Subject", fmt.Sprintf("Remailer key for %s", cfg.Remailer.Name)) 577 | m.Filename = cfg.Files.Pubkey 578 | m.Prefix = "Here is the Mixmaster key:\n\n=-=-=-=-=-=-=-=-=-=-=-=" 579 | } else if strings.HasPrefix(subject, "remailer-conf") { 580 | // remailer-conf 581 | log.Tracef("remailer-conf request from %s", sender) 582 | m.Set( 583 | "Subject", 584 | fmt.Sprintf("Capabilities of the %s remailer", cfg.Remailer.Name)) 585 | m.Text(fmt.Sprintf("Remailer-Type: Mixmaster %s\n", version)) 586 | m.Text("Supported Formats:\n Mixmaster\n") 587 | m.Text(fmt.Sprintf("Pool size: %d\n", cfg.Pool.Size)) 588 | m.Text(fmt.Sprintf("Maximum message size: %d kB\n", cfg.Remailer.MaxSize)) 589 | m.Text("The following header lines will be filtered:\n") 590 | m.Text( 591 | fmt.Sprintf("\n$remailer{\"%s\"} = \"<%s>", 592 | cfg.Remailer.Name, cfg.Remailer.Address)) 593 | if !cfg.Remailer.Exit { 594 | m.Text(" middle") 595 | } 596 | packetVersions := []string{"v2"} 597 | for _, v := range packetVersions { 598 | m.Text(fmt.Sprintf(" %s", v)) 599 | } 600 | m.Text("\";\n") 601 | m.Text("\nSUPPORTED MIXMASTER (TYPE II) REMAILERS") 602 | var pubList []string 603 | pubList, err := keymgr.Headers(cfg.Files.Pubring) 604 | if err != nil { 605 | log.Infof("Could not read %s", cfg.Files.Pubring) 606 | } else { 607 | m.List(pubList) 608 | } 609 | } else if strings.HasPrefix(subject, "remailer-adminkey") { 610 | // remailer-adminkey 611 | log.Tracef("remailer-adminkey request from %s", sender) 612 | m.Set( 613 | "Subject", 614 | fmt.Sprintf("Admin key for the %s remailer", cfg.Remailer.Name)) 615 | m.Filename = cfg.Files.Adminkey 616 | } else if strings.HasPrefix(subject, "remailer-help") { 617 | // remailer-help 618 | log.Tracef("remailer-help request from %s", sender) 619 | m.Set( 620 | "Subject", 621 | fmt.Sprintf("Your help request for the %s Anonymous Remailer", 622 | cfg.Remailer.Name)) 623 | m.Filename = cfg.Files.Help 624 | } else { 625 | if len(subject) > 20 { 626 | // Truncate long subject headers before logging them 627 | subject = subject[:20] 628 | } 629 | err = fmt.Errorf("ignoring request for %s", subject) 630 | return 631 | } 632 | var msg []byte 633 | msg, err = m.Compile() 634 | if err != nil { 635 | log.Infof("Unable to send %s", subject) 636 | return 637 | } 638 | err = mailBytes(msg, []string{sender}) 639 | if err != nil { 640 | log.Warnf("Failed to send %s to %s", subject, sender) 641 | return 642 | } 643 | return 644 | } 645 | -------------------------------------------------------------------------------- /stats.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/Masterminds/log-go" 7 | ) 8 | 9 | type statistics struct { 10 | inDummy int 11 | inMail int 12 | inRemFoo int 13 | inYamn int 14 | outDummy int 15 | outMail int 16 | outYamn int 17 | outLoop int 18 | outRandhop int 19 | outPlain int 20 | } 21 | 22 | func (s *statistics) reset() { 23 | s.inDummy = 0 24 | s.inMail = 0 25 | s.inYamn = 0 26 | s.inRemFoo = 0 27 | s.outDummy = 0 28 | s.outMail = 0 29 | s.outYamn = 0 30 | s.outLoop = 0 31 | s.outRandhop = 0 32 | s.outPlain = 0 33 | log.Info("Daily stats reset") 34 | } 35 | 36 | func (s *statistics) report() { 37 | log.Infof( 38 | "MailIn=%d, RemFoo=%d, YamnIn=%d, DummyIn=%d", 39 | s.inMail, 40 | s.inRemFoo, 41 | s.inYamn, 42 | s.inDummy, 43 | ) 44 | line1 := fmt.Sprintf( 45 | "MailOut=%d, YamnOut=%d, YamnLoop=%d, Randhop=%d, ", 46 | s.outMail, 47 | s.outYamn, 48 | s.outLoop, 49 | s.outRandhop, 50 | ) 51 | line2 := fmt.Sprintf( 52 | "FinalOut=%d, DummyOut=%d", 53 | s.outPlain, 54 | s.outDummy, 55 | ) 56 | log.Infof(line1 + line2) 57 | } 58 | 59 | var stats = new(statistics) 60 | -------------------------------------------------------------------------------- /utils.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "encoding/base64" 7 | "encoding/hex" 8 | "errors" 9 | "fmt" 10 | "io" 11 | "io/ioutil" 12 | "net/http" 13 | "os" 14 | "strconv" 15 | "strings" 16 | "time" 17 | 18 | "github.com/Masterminds/log-go" 19 | "github.com/crooks/yamn/crandom" 20 | "github.com/crooks/yamn/linebreaker" 21 | "github.com/dchest/blake2s" 22 | //"github.com/codahale/blake2" 23 | ) 24 | 25 | // min returns the lower of two integers 26 | func min(x, y int) int { 27 | if x < y { 28 | return x 29 | } 30 | return y 31 | } 32 | 33 | // max returns the higher of two integers 34 | func max(x, y int) int { 35 | if x > y { 36 | return x 37 | } 38 | return y 39 | } 40 | 41 | // daysAgo takes a timestamp and returns an integer of its age in days. 42 | func daysAgo(date time.Time) (days int) { 43 | age := time.Since(date) 44 | days = int(age.Hours() / 24) 45 | return 46 | } 47 | 48 | // IsMemberStr tests for the membership of a string in a slice 49 | func IsMemberStr(s string, slice []string) bool { 50 | for _, n := range slice { 51 | if n == s { 52 | return true 53 | } 54 | } 55 | return false 56 | } 57 | 58 | // readdir returns a list of files in a specified directory that begin with 59 | // the specified prefix. 60 | func readDir(path, prefix string) (files []string, err error) { 61 | fi, err := ioutil.ReadDir(path) 62 | if err != nil { 63 | return 64 | } 65 | for _, f := range fi { 66 | if !f.IsDir() && strings.HasPrefix(f.Name(), prefix) { 67 | files = append(files, f.Name()) 68 | } 69 | } 70 | return 71 | } 72 | 73 | // messageID returns an RFC compliant Message-ID for use in message 74 | // construction. 75 | func messageID() (datestr string) { 76 | dateComponent := time.Now().Format("20060102.150405") 77 | randomComponent := hex.EncodeToString(crandom.Randbytes(4)) 78 | var domainComponent string 79 | if strings.Contains(cfg.Remailer.Address, "@") { 80 | domainComponent = strings.SplitN( 81 | cfg.Remailer.Address, "@", 2, 82 | )[1] 83 | } else { 84 | domainComponent = "yamn.invalid" 85 | } 86 | datestr = fmt.Sprintf( 87 | "<%s.%s@%s>", 88 | dateComponent, 89 | randomComponent, 90 | domainComponent, 91 | ) 92 | return 93 | } 94 | 95 | // lenCheck verifies that a slice is of a specified length 96 | func lenCheck(got, expected int) (err error) { 97 | if got != expected { 98 | err = fmt.Errorf("incorrect length: Expected=%d, Got=%d", expected, got) 99 | log.Info(err) 100 | } 101 | return 102 | } 103 | 104 | // bufLenCheck verifies that a given buffer length is of a specified length 105 | func bufLenCheck(buflen, length int) (err error) { 106 | if buflen != length { 107 | err = fmt.Errorf("incorrect buffer length: Wanted=%d, Got=%d", length, buflen) 108 | log.Info(err) 109 | } 110 | return 111 | } 112 | 113 | // Return the time when filename was last modified 114 | func fileTime(filename string) (t time.Time, err error) { 115 | info, err := os.Stat(filename) 116 | if err != nil { 117 | return 118 | } 119 | t = info.ModTime() 120 | return 121 | } 122 | 123 | // httpGet retrieves url and stores it in filename 124 | func httpGet(url, filename string) (err error) { 125 | res, err := http.Get(url) 126 | if err != nil { 127 | return 128 | } 129 | if res.StatusCode < 200 || res.StatusCode > 299 { 130 | err = fmt.Errorf("%s: %s", url, res.Status) 131 | return err 132 | } 133 | content, err := ioutil.ReadAll(res.Body) 134 | if err != nil { 135 | return 136 | } 137 | err = ioutil.WriteFile(filename, content, 0644) 138 | if err != nil { 139 | return 140 | } 141 | return 142 | } 143 | 144 | // isPath returns True if a given file or directory exists 145 | func isPath(path string) (bool, error) { 146 | var err error 147 | _, err = os.Stat(path) 148 | if err == nil { 149 | return true, nil 150 | } 151 | if os.IsNotExist(err) { 152 | return false, nil 153 | } 154 | return false, err 155 | } 156 | 157 | // assertExists panics if a given file or dir doesn't exist 158 | func assertIsPath(path string) { 159 | testPath, err := isPath(path) 160 | if err != nil { 161 | // Some error occurred other than the path not existing 162 | panic(err) 163 | } 164 | if !testPath { 165 | // Arghh, the path doesn't exist! 166 | err = fmt.Errorf("assertion failure: path %s does not exist", path) 167 | panic(err) 168 | } 169 | } 170 | 171 | // sPopBytes returns n bytes from the start of a slice 172 | func sPopBytes(sp *[]byte, n int) (pop []byte, err error) { 173 | s := *sp 174 | if len(s) < n { 175 | err = fmt.Errorf("cannot pop %d bytes from slice of %d", n, len(s)) 176 | return 177 | } 178 | pop = s[:n] 179 | s = s[n:] 180 | *sp = s 181 | return 182 | } 183 | 184 | // ePopBytes returns n bytes from the end of a slice 185 | func ePopBytes(sp *[]byte, n int) (pop []byte, err error) { 186 | s := *sp 187 | if len(s) < n { 188 | err = fmt.Errorf("cannot pop %d bytes from slice of %d", n, len(s)) 189 | return 190 | } 191 | pop = s[len(s)-n:] 192 | s = s[:len(s)-n] 193 | *sp = s 194 | return 195 | } 196 | 197 | // popstr takes a pointer to a string slice and pops the last element 198 | func popstr(s *[]string) (element string) { 199 | slice := *s 200 | element, slice = slice[len(slice)-1], slice[:len(slice)-1] 201 | *s = slice 202 | return 203 | } 204 | 205 | // writeInternalHeader inserts a Yamn internal header containing the pooled 206 | // date. This is useful for performing expiry on old messages. 207 | func writeInternalHeader(w io.Writer) { 208 | dateHeader := fmt.Sprintf( 209 | "Yamn-Pooled-Date: %s\n", 210 | time.Now().Format(shortdate), 211 | ) 212 | w.Write([]byte(dateHeader)) 213 | } 214 | 215 | func writeMailHeaders(w io.Writer, sendTo string) { 216 | w.Write([]byte(fmt.Sprintf("To: %s\n", sendTo))) 217 | w.Write([]byte(fmt.Sprintf("From: %s\n", cfg.Remailer.Address))) 218 | w.Write([]byte(fmt.Sprintf("Subject: yamn-%s\n", version))) 219 | w.Write([]byte("\n")) 220 | } 221 | 222 | // wrap64 writes a byte payload as wrapped base64 to an io.writer 223 | func wrap64(writer io.Writer, b []byte, wrap int) { 224 | breaker := linebreaker.NewLineBreaker(writer, wrap) 225 | b64 := base64.NewEncoder(base64.StdEncoding, breaker) 226 | b64.Write(b) 227 | b64.Close() 228 | breaker.Close() 229 | } 230 | 231 | // armor converts a plain-byte Yamn message to a Base64 armored message with 232 | // cutmarks and header fields. 233 | func armor(w io.Writer, payload []byte) { 234 | err := lenCheck(len(payload), messageBytes) 235 | if err != nil { 236 | panic(err) 237 | } 238 | w.Write([]byte("::\n")) 239 | w.Write([]byte(fmt.Sprintf("Remailer-Type: yamn-%s\n\n", version))) 240 | w.Write([]byte("-----BEGIN REMAILER MESSAGE-----\n")) 241 | // Write message length 242 | w.Write([]byte(strconv.Itoa(len(payload)) + "\n")) 243 | //digest := blake2.New(&blake2.Config{Size: 16}) 244 | digest, err := blake2s.New(nil) 245 | if err != nil { 246 | panic(err) 247 | } 248 | digest.Write(payload) 249 | // Write message digest 250 | w.Write([]byte(hex.EncodeToString(digest.Sum(nil)) + "\n")) 251 | // Write the payload to the base64 wrapper 252 | wrap64(w, payload, base64LineWrap) 253 | w.Write([]byte("\n-----END REMAILER MESSAGE-----\n")) 254 | } 255 | 256 | // stripArmor takes a Mixmaster formatted message from an ioreader and 257 | // returns its payload as a byte slice 258 | func stripArmor(reader io.Reader) (payload []byte, err error) { 259 | scanner := bufio.NewScanner(reader) 260 | scanPhase := 0 261 | b64 := new(bytes.Buffer) 262 | var statedLen int 263 | var payloadDigest []byte 264 | /* Scan phases are: 265 | 0 Expecting :: 266 | 1 Expecting Begin cutmarks 267 | 2 Expecting size 268 | 3 Expecting hash 269 | 4 In payload and checking for End cutmark 270 | 5 Got End cutmark 271 | */ 272 | for scanner.Scan() { 273 | line := scanner.Text() 274 | switch scanPhase { 275 | case 0: 276 | // Expecting ::\n 277 | if line == "::" { 278 | scanPhase = 1 279 | continue 280 | } 281 | case 1: 282 | // Expecting Begin cutmarks 283 | if line == "-----BEGIN REMAILER MESSAGE-----" { 284 | scanPhase = 2 285 | } 286 | case 2: 287 | // Expecting size 288 | statedLen, err = strconv.Atoi(line) 289 | if err != nil { 290 | err = fmt.Errorf("unable to extract payload size from %s", line) 291 | return 292 | } 293 | scanPhase = 3 294 | case 3: 295 | if len(line) != 64 { 296 | err = fmt.Errorf("expected 64 digit Hex encoded Hash, got %d bytes", len(line)) 297 | return 298 | } 299 | payloadDigest, err = hex.DecodeString(line) 300 | if err != nil { 301 | err = errors.New("unable to decode Hex hash on payload") 302 | return 303 | } 304 | scanPhase = 4 305 | case 4: 306 | if line == "-----END REMAILER MESSAGE-----" { 307 | scanPhase = 5 308 | break 309 | } 310 | b64.WriteString(line) 311 | } // End of switch 312 | } // End of file scan 313 | switch scanPhase { 314 | case 0: 315 | err = errors.New("no :: found on message") 316 | return 317 | case 1: 318 | err = errors.New("no Begin cutmarks found on message") 319 | return 320 | case 4: 321 | err = errors.New("no End cutmarks found on message") 322 | return 323 | } 324 | payload = make([]byte, base64.StdEncoding.DecodedLen(b64.Len())) 325 | payloadLen, err := base64.StdEncoding.Decode(payload, b64.Bytes()) 326 | if err != nil { 327 | return 328 | } 329 | // Tuncate payload to the number of decoded bytes 330 | payload = payload[0:payloadLen] 331 | // Validate payload length against stated length. 332 | if statedLen != payloadLen { 333 | err = fmt.Errorf("payload size doesn't match stated size. Stated=%d, Got=%d", statedLen, payloadLen) 334 | return 335 | } 336 | // Validate payload length against packet format. 337 | if payloadLen != messageBytes { 338 | err = fmt.Errorf("payload size doesn't match stated size. Wanted=%d, Got=%d", messageBytes, payloadLen) 339 | return 340 | } 341 | //digest := blake2.New(&blake2.Config{Size: 16}) 342 | digest, err := blake2s.New(nil) 343 | if err != nil { 344 | panic(err) 345 | } 346 | digest.Write(payload) 347 | if !bytes.Equal(digest.Sum(nil), payloadDigest) { 348 | err = errors.New("incorrect payload digest during dearmor") 349 | return 350 | } 351 | return 352 | } 353 | -------------------------------------------------------------------------------- /www/yamn_packet.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | YAMN Packet Specification 6 | 7 | 8 | 9 | 10 |

Yamn Packet Specification

11 |

12 | Yamn (Yet Another Mix Network) is a very close relative to Mixmaster. 13 | The keyrings, stats and payload format are similar enough that the 14 | Echolot Pinger can be used with only minor modification to some Regular 15 | Expressions. 16 |

17 |

18 | The following table presents a simple overview of the principle 19 | differences between Yamn and Mixmaster. 20 |

21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 |
FunctionMixmasterYamn
Public Key EncryptionRSANaCl Box
Symmetric EncryptionDES3AES256 (CTR Mode)
Integrity CheckingMD5 (Partial only)Blake2 (ed25519 with NaCl)
Pooling AlgorithmDynamic MixBinomial Mix
Maximum Chain Length2010
53 |

Payload Format

54 |

55 | The term payload describes the entire Yamn payload at it's highest 56 | level. The Headers content is described in greater detail in 57 | subsequent sections. During client-side message construction, all 58 | headers (excluding the first one) and the Body are wrapped in multiple 59 | layers of encryption. These layers are stripped, one-by-one at each 60 | hop until the plain content is exposed for delivery to the actual 61 | recipient. 62 |

63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 |
Field NameBytesDescription
Headers256010 x Headers (1 per hop)
Body17920User defined content
Total20480
85 |

Header Format

86 |

87 | Each of the 10 headers consists of 256 Bytes split into 4 fields. The 88 | first 3 fields contain instructions on how to decrypt the content of 89 | the 4th field, the Sealed Header. 90 |

91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 |
Field NameBytesDescription
Recipient key ID16KeyID required to decrypt NaCl Sealed header
Sender Public key32Public key corresponding to SK used in NaCl Seal
Xsalsa20 Nonce24Nonce used during NaCl Seal
NaCl Sealed header176160 Bytes + NaCl Overhead
Random padding8
Total256
128 |

129 | The client generates a NaCl key pair for each hop in the chain. The SK 130 | is used to encrypt the Sealed Header and is then discarded. The PK is 131 | transmitted within the header. The KeyID informs the recipient 132 | Remailer of what SK to use for decryption. 133 |

134 |

NaCl Sealed Header Format

135 |

136 | The Sealed Header contains sensitive content, such as how to decrypt 137 | other payload components and further delivery instructions. 138 |

139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 |
Field NameBytesDescription
Packet version1Support for multiple packet versions.
Packet Type ID1Intermediate Hops=0, Exit Hop=1
Delivery Protocol1SMTP=0 (Currently the only option)
Packet ID16Unique packet identifier (prevents replay)
AES-CTR Key32AES Key required to decrypt subsequent headers and body
Timestamp2Days since Epoch in Little-Endian 174 | format. A random 0-3 days are subtracted during 175 | stamp creation.
Packet Info64Packet-Type specific headers
Anti-Tag Digest32Blake2 Digest of subsequent headers and body
Padding11\x00 Bytes (encrypted)
Total160
198 |

Packet Info - Intermediate Hop

199 |

200 | Sealed headers that are specific to Intermediate type hops. 201 |

202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 |
Field NameBytesDescription
Partial AES-CTR IV12Random 12 Bytes of 16 Byte IV
Next Hop Address52Address of next hop
Total64
224 |

225 | IV's are constructed from 12 random bytes and a 4 Byte counter in 226 | Little-Endian format. The top-most header uses sequence 0, the 227 | bottom-most header uses sequence 8 and the Body, sequence 9. The 228 | format is: RRRRNNNNRRRRRRRR. 229 |

230 |

Packet Info - Exit Hop

231 |

232 | Sealed headers that are specific to the Exit hops. Each complete chain 233 | can only contain a single Exit Hop. 234 |

235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 |
Field NameBytesDescription
AES-CTR IV16Used to decrypt Body
Chunk num1Sequence number for chunked messages
Total number of chunks1Number of chunks required for complete Body
Message-ID16Common to all chunks of a message
Body length4Total length of Body (less padding) in 265 | Little-Endian format
Delivery Method1Delivery Protocol. 0=SMTP, 255=Dummy
Padding25\x00 Bytes (encrypted)
Total64
283 | 284 | 285 | 286 | -------------------------------------------------------------------------------- /yamn.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | stdlog "log" 7 | "os" 8 | 9 | "github.com/Masterminds/log-go" 10 | "github.com/crooks/jlog" 11 | "github.com/crooks/yamn/config" 12 | "github.com/crooks/yamn/idlog" 13 | "github.com/crooks/yamn/keymgr" 14 | "github.com/luksen/maildir" 15 | ) 16 | 17 | const ( 18 | version string = "0.2.6" 19 | dayLength int = 24 * 60 * 60 // Day in seconds 20 | maxFragLength = 17910 21 | maxCopies = 5 22 | base64LineWrap = 64 23 | rfc5322date = "Mon, 2 Jan 2006 15:04:05 -0700" 24 | shortdate = "2 Jan 2006" 25 | ) 26 | 27 | var ( 28 | // flags - Command line flags 29 | flag *config.Flags 30 | // cfg - Config parameters 31 | cfg *config.Config 32 | // Pubring - Public Keyring 33 | Pubring *keymgr.Pubring 34 | // IDDb - Message ID log (replay protection) 35 | IDDb *idlog.IDLog 36 | // ChunkDb - Chunk database 37 | ChunkDb *Chunk 38 | ) 39 | 40 | func main() { 41 | var err error 42 | flag, cfg = config.GetCfg() 43 | if flag.Version { 44 | fmt.Println(version) 45 | os.Exit(0) 46 | } 47 | // If the debug flag is set, print the config and exit 48 | if flag.Debug { 49 | y, err := cfg.Debug() 50 | if err != nil { 51 | fmt.Printf("Debugging Error: %s\n", err) 52 | os.Exit(1) 53 | } 54 | fmt.Printf("%s\n", y) 55 | os.Exit(0) 56 | } 57 | 58 | // Set up logging 59 | loglevel, err := log.Atoi(cfg.General.Loglevel) 60 | if err != nil { 61 | fmt.Fprintf(os.Stderr, "%s: Unknown loglevel", cfg.General.Loglevel) 62 | os.Exit(1) 63 | } 64 | // If we're logging to a file, open the file and redirect output to it 65 | if cfg.General.LogToFile && cfg.General.LogToJournal { 66 | fmt.Fprintln(os.Stderr, "Cannot log to file and journal") 67 | os.Exit(1) 68 | } else if cfg.General.LogToFile { 69 | logfile, err := os.OpenFile(cfg.Files.Logfile, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0644) 70 | if err != nil { 71 | fmt.Fprintf(os.Stderr, "%s: Error opening logfile: %v", cfg.Files.Logfile, err) 72 | os.Exit(1) 73 | } 74 | stdlog.SetOutput(logfile) 75 | log.Current = log.StdLogger{Level: loglevel} 76 | } else if cfg.General.LogToJournal { 77 | log.Current = jlog.NewJournal(loglevel) 78 | } else { 79 | log.Current = log.StdLogger{Level: loglevel} 80 | } 81 | 82 | // Inform the user which (if any) config file was used. 83 | if cfg.Files.Config != "" { 84 | log.Infof("Using config file: %s", cfg.Files.Config) 85 | } else { 86 | log.Warn("No config file was found. Resorting to defaults") 87 | } 88 | 89 | // Setup complete, time to do some work 90 | if flag.Client { 91 | mixprep() 92 | } else if flag.Stdin { 93 | dir := maildir.Dir(cfg.Files.Maildir) 94 | newmsg, err := dir.NewDelivery() 95 | if err != nil { 96 | fmt.Fprintln(os.Stderr, err) 97 | os.Exit(1) 98 | } 99 | stdin, err := ioutil.ReadAll(os.Stdin) 100 | if err != nil { 101 | fmt.Fprintln(os.Stderr, err) 102 | os.Exit(1) 103 | } 104 | newmsg.Write(stdin) 105 | newmsg.Close() 106 | } else if flag.Remailer { 107 | err = loopServer() 108 | if err != nil { 109 | panic(err) 110 | } 111 | } else if flag.Dummy { 112 | injectDummy() 113 | } else if flag.Refresh { 114 | fmt.Printf("Keyring refresh: from=%s, to=%s\n", cfg.Urls.Pubring, cfg.Files.Pubring) 115 | httpGet(cfg.Urls.Pubring, cfg.Files.Pubring) 116 | fmt.Printf("Stats refresh: from=%s, to=%s\n", cfg.Urls.Mlist2, cfg.Files.Mlist2) 117 | httpGet(cfg.Urls.Mlist2, cfg.Files.Mlist2) 118 | } 119 | if flag.Send { 120 | // Flush the outbound pool 121 | poolOutboundSend() 122 | } 123 | } 124 | --------------------------------------------------------------------------------