├── LICENSE ├── receive_test.go ├── ratchet.go ├── send.go ├── main.go ├── receive.go ├── pkg └── ratchet │ ├── ratchet_test.go │ └── ratchet.go └── README.md /LICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /receive_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "testing" 4 | 5 | func TestBlockSplitter(t *testing.T) { 6 | b := newBlockSplitter([]byte(`-----BEGIN KEY EXCHANGE MATERIAL----- 7 | 8 | eyJpZHB1YiI6IjM4YmVlZDI1ZjAzYmNjMDhkN2E1YTJkNzEwMWUwNWVlNGJiNGYz 9 | ZTU4MGMxYWI2MjQzNmYyN2ViN2ZiZGVkNWMiLCJkaCI6IjQxMWY2MDNmYWEyODE4 10 | ODgxMWU3NDIzZTNjZjgzNzc4M2M2N2FjYjhmYjRiNjNmNDUxZWVkMDgyOGU5YWI0 11 | NDQiLCJkaDEiOiI2Yjg4MjQxYjNhNjViNDIxOWM4YWNhMWNiYjE1OWM2OWIxOWRk 12 | NTFlNTZkNTljNmZmZDgzYWU2OWQyMWYzMDM5In0K 13 | =AC6N 14 | -----END KEY EXCHANGE MATERIAL----------BEGIN GOAX ENCRYPTED MESSAGE----- 15 | 16 | /daa6armIWZ226N+WaDCkyIE9XuiNbRJ0tNWPqtnNTcA9M6NxPcmppEB5tRYbOvs 17 | xuldSH8eLU+slSZcuHhWZSHj72tM6dFm2ZvUQmwxVo7xXT8VRJC4fUyjMtW7LwUG 18 | 9hQWFD4p22m5PiXfEOWSZFCDhI+pu4Puy88MZvXI1g== 19 | =uSJC 20 | -----END GOAX ENCRYPTED MESSAGE-----`)) 21 | 22 | if !b.Scan() { 23 | t.Fatal("Scanner didn't advance") 24 | } 25 | firstBlock := b.Text() 26 | expectedFirstBlock := `-----BEGIN KEY EXCHANGE MATERIAL----- 27 | 28 | eyJpZHB1YiI6IjM4YmVlZDI1ZjAzYmNjMDhkN2E1YTJkNzEwMWUwNWVlNGJiNGYz 29 | ZTU4MGMxYWI2MjQzNmYyN2ViN2ZiZGVkNWMiLCJkaCI6IjQxMWY2MDNmYWEyODE4 30 | ODgxMWU3NDIzZTNjZjgzNzc4M2M2N2FjYjhmYjRiNjNmNDUxZWVkMDgyOGU5YWI0 31 | NDQiLCJkaDEiOiI2Yjg4MjQxYjNhNjViNDIxOWM4YWNhMWNiYjE1OWM2OWIxOWRk 32 | NTFlNTZkNTljNmZmZDgzYWU2OWQyMWYzMDM5In0K 33 | =AC6N 34 | -----END KEY EXCHANGE MATERIAL-----` 35 | 36 | if firstBlock != expectedFirstBlock { 37 | t.Fatalf("invalid first block, got %v, expected %v", firstBlock, expectedFirstBlock) 38 | } 39 | 40 | if !b.Scan() { 41 | t.Fatal("Scanner didn't advance") 42 | } 43 | secondBlock := b.Text() 44 | expectedSecondBlock := `-----BEGIN GOAX ENCRYPTED MESSAGE----- 45 | 46 | /daa6armIWZ226N+WaDCkyIE9XuiNbRJ0tNWPqtnNTcA9M6NxPcmppEB5tRYbOvs 47 | xuldSH8eLU+slSZcuHhWZSHj72tM6dFm2ZvUQmwxVo7xXT8VRJC4fUyjMtW7LwUG 48 | 9hQWFD4p22m5PiXfEOWSZFCDhI+pu4Puy88MZvXI1g== 49 | =uSJC 50 | -----END GOAX ENCRYPTED MESSAGE-----` 51 | 52 | if secondBlock != expectedSecondBlock { 53 | t.Fatalf("invalid second block, got %v, expected %v", secondBlock, expectedSecondBlock) 54 | } 55 | 56 | if b.Scan() { 57 | t.Fatal("Shouldn't advance a third time") 58 | } 59 | if err := b.Err(); err != nil { 60 | t.Fatal("Got an error after all scanning: ", err) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /ratchet.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "crypto/rand" 5 | "encoding/hex" 6 | "encoding/json" 7 | "os" 8 | "path" 9 | 10 | "github.com/pkg/errors" 11 | "github.com/rakoo/goax/pkg/ratchet" 12 | "golang.org/x/crypto/openpgp/armor" 13 | ) 14 | 15 | var errNoRatchet = errors.New("No ratchet") 16 | 17 | var errInvalidRatchet = errors.New("Invalid ratchet") 18 | 19 | func openRatchet(peer string) (r *ratchet.Ratchet, err error) { 20 | f, err := os.Open(path.Join("ratchets", hex.EncodeToString([]byte(peer)))) 21 | if err != nil { 22 | return nil, errNoRatchet 23 | } 24 | defer f.Close() 25 | 26 | myIdentityKeyPrivate := getPrivateKey() 27 | var asArray [32]byte 28 | copy(asArray[:], myIdentityKeyPrivate) 29 | r = ratchet.New(rand.Reader, asArray) 30 | 31 | armorDecoder, err := armor.Decode(f) 32 | if err != nil { 33 | return nil, errors.Wrap(err, "Error opening decoder") 34 | } 35 | err = json.NewDecoder(armorDecoder.Body).Decode(r) 36 | if err != nil { 37 | return nil, errInvalidRatchet 38 | } 39 | 40 | return r, nil 41 | } 42 | 43 | func createRatchet(peer string) (r *ratchet.Ratchet, err error) { 44 | myIdentityKeyPrivate := getPrivateKey() 45 | var asArray [32]byte 46 | copy(asArray[:], myIdentityKeyPrivate) 47 | r = ratchet.New(rand.Reader, asArray) 48 | err = saveRatchet(r, peer) 49 | markAsNew(peer) 50 | return r, err 51 | } 52 | 53 | func saveRatchet(r *ratchet.Ratchet, peer string) error { 54 | os.MkdirAll("ratchets", 0755) 55 | f, err := os.Create(path.Join("ratchets", hex.EncodeToString([]byte(peer)))) 56 | if err != nil { 57 | return errors.Wrap(err, "Couldn't create ratchet file") 58 | } 59 | defer f.Close() 60 | 61 | armorEncoder, err := armor.Encode(f, "GOAX RATCHET", nil) 62 | if err != nil { 63 | return errors.Wrap(err, "Couldn't create armor encoder") 64 | } 65 | err = json.NewEncoder(armorEncoder).Encode(r) 66 | if err != nil { 67 | return errors.Wrap(err, "Couldn't marshall ratchet") 68 | } 69 | err = armorEncoder.Close() 70 | if err != nil { 71 | return errors.Wrap(err, "Couldn't close armor encoder") 72 | } 73 | return nil 74 | } 75 | 76 | func markAsNew(peer string) { 77 | os.MkdirAll("new", 0755) 78 | os.Create(path.Join("new", hex.EncodeToString([]byte(peer)))) 79 | } 80 | 81 | func isNew(peer string) bool { 82 | f, err := os.Open(path.Join("new", hex.EncodeToString([]byte(peer)))) 83 | defer f.Close() 84 | 85 | return err == nil 86 | } 87 | 88 | func deleteNew(peer string) { 89 | os.Remove(path.Join("new", hex.EncodeToString([]byte(peer)))) 90 | } 91 | -------------------------------------------------------------------------------- /send.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "encoding/hex" 6 | "encoding/json" 7 | "fmt" 8 | "io" 9 | "io/ioutil" 10 | "log" 11 | "os" 12 | "path" 13 | 14 | "github.com/rakoo/goax/pkg/ratchet" 15 | 16 | "golang.org/x/crypto/openpgp/armor" 17 | ) 18 | 19 | func send(peer string) { 20 | r, err := openRatchet(peer) 21 | if err != nil { 22 | if err == errNoRatchet { 23 | fmt.Fprintf(os.Stderr, "No ratchet for %s, please send this to the peer and \"receive\" what they send you back", peer) 24 | fmt.Println("\n") 25 | r, err := createRatchet(peer) 26 | if err != nil { 27 | log.Fatalf("Couldn't create ratchet for %s: %s", peer, err) 28 | } 29 | err = saveRatchet(r, peer) 30 | if err != nil { 31 | log.Fatal("Couldn't save ratchet, will have to try another time", err) 32 | } 33 | sendRatchet(r) 34 | fmt.Println("") 35 | return 36 | } else { 37 | log.Fatal(err) 38 | } 39 | } 40 | 41 | fmt.Println("") 42 | msg, err := ioutil.ReadAll(os.Stdin) 43 | if err != nil { 44 | log.Fatal("Couldn't read all stdin") 45 | } 46 | fmt.Println("") 47 | cipherText, err := r.Encrypt(msg) 48 | if err != nil { 49 | if err == ratchet.ErrHandshakeNotComplete { 50 | fmt.Fprintf(os.Stderr, "\nSorry, the handshake is not complete yet; you can't send any messages. Please ask %s for their key exchange material and use \"goax receive %s\" to finish handshake.\n", peer, peer) 51 | fmt.Fprintf(os.Stderr, "Here's your own key exchange material, in case you want to send it again to them:\n\n") 52 | sendRatchet(r) 53 | os.Exit(1) 54 | } else { 55 | log.Fatal(err) 56 | } 57 | } 58 | 59 | if err := saveRatchet(r, peer); err != nil { 60 | log.Println("Couldn't save ratchet:", err) 61 | os.Remove(path.Join("ratchets", hex.EncodeToString([]byte(peer)))) 62 | os.Exit(1) 63 | } 64 | 65 | if isNew(peer) { 66 | sendRatchet(r) 67 | } 68 | 69 | encoder, err := armor.Encode(os.Stdout, ENCRYPTED_MESSAGE_TYPE, nil) 70 | if err != nil { 71 | log.Fatal("Couldn't create armor encoder: ", err) 72 | } 73 | 74 | io.Copy(encoder, bytes.NewReader(cipherText)) 75 | encoder.Close() 76 | fmt.Println("") 77 | } 78 | 79 | func sendRatchet(r *ratchet.Ratchet) { 80 | kx, err := r.GetKeyExchangeMaterial() 81 | if err != nil { 82 | log.Fatal("Couldn't get key exchange material ", err) 83 | } 84 | encoder, err := armor.Encode(os.Stdout, KEY_EXCHANGE_TYPE, nil) 85 | if err != nil { 86 | log.Fatal("Couldn't get armor encoder") 87 | } 88 | 89 | json.NewEncoder(encoder).Encode(kx) 90 | encoder.Close() 91 | fmt.Println("") 92 | } 93 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "crypto/rand" 5 | "fmt" 6 | "io" 7 | "io/ioutil" 8 | "log" 9 | "os" 10 | 11 | "golang.org/x/crypto/curve25519" 12 | "golang.org/x/crypto/openpgp/armor" 13 | 14 | "github.com/crowsonkb/base58" 15 | "github.com/pkg/errors" 16 | ) 17 | 18 | func main() { 19 | if len(os.Args) < 2 { 20 | fmt.Println("Need an action: one of mykey, send or receive") 21 | os.Exit(1) 22 | } 23 | 24 | ensureIdentityKey() 25 | 26 | switch os.Args[1] { 27 | case "mykey": 28 | printPublicKey() 29 | case "send": 30 | if len(os.Args) < 3 { 31 | fmt.Println("Need email adress of recipient") 32 | os.Exit(1) 33 | } 34 | send(os.Args[2]) 35 | case "receive": 36 | if len(os.Args) < 3 { 37 | fmt.Println("Need email adress of sender") 38 | os.Exit(1) 39 | } 40 | receive(os.Args[2]) 41 | default: 42 | fmt.Println("Unrecognized action:", os.Args[1]) 43 | fmt.Println("Need one of generate, send or receive") 44 | os.Exit(1) 45 | } 46 | } 47 | 48 | func ensureIdentityKey() { 49 | _, err := os.Open("key") 50 | if err != nil { 51 | f, err := os.Create("key") 52 | if err != nil { 53 | log.Fatal(errors.Wrap(err, "Couldn't create private identity key")) 54 | } 55 | encoder, err := armor.Encode(f, "GOAX PRIVATE KEY", nil) 56 | if err != nil { 57 | log.Fatal(errors.Wrap(err, "Couldn't create armored writer")) 58 | } 59 | _, err = io.CopyN(encoder, rand.Reader, 32) 60 | if err != nil { 61 | log.Fatal(errors.Wrap(err, "Couldn't write private key to file")) 62 | } 63 | err = encoder.Close() 64 | if err != nil { 65 | log.Fatal(errors.Wrap(err, "Couldn't close encoder")) 66 | } 67 | err = f.Close() 68 | if err != nil { 69 | log.Fatal(errors.Wrap(err, "Couldn't close file")) 70 | } 71 | } 72 | } 73 | 74 | func printPublicKey() { 75 | var myPublicKey [32]byte 76 | var myPrivateKey [32]byte 77 | copy(myPrivateKey[:], getPrivateKey()) 78 | curve25519.ScalarBaseMult(&myPublicKey, &myPrivateKey) 79 | fmt.Println(base58.Encode(myPublicKey[:])) 80 | } 81 | 82 | func getPrivateKey() (pkey []byte) { 83 | f, err := os.Open("key") 84 | if err != nil { 85 | log.Fatal(errors.Wrap(err, "Error opening private key")) 86 | } 87 | defer f.Close() 88 | 89 | block, err := armor.Decode(f) 90 | if err != nil { 91 | log.Fatal(errors.Wrap(err, "Error decoding private key")) 92 | } 93 | private, err := ioutil.ReadAll(block.Body) 94 | if err != nil { 95 | log.Fatal(errors.Wrap(err, "Error decoding private key")) 96 | } 97 | return private 98 | } 99 | 100 | const ( 101 | ENCRYPTED_MESSAGE_TYPE string = "GOAX ENCRYPTED MESSAGE" 102 | KEY_EXCHANGE_TYPE = "KEY EXCHANGE MATERIAL" 103 | ) 104 | -------------------------------------------------------------------------------- /receive.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "encoding/json" 7 | "fmt" 8 | "io" 9 | "io/ioutil" 10 | "log" 11 | "os" 12 | "strings" 13 | 14 | "github.com/rakoo/goax/pkg/ratchet" 15 | "golang.org/x/crypto/openpgp/armor" 16 | ) 17 | 18 | func receive(peer string) { 19 | getRatchet := func(peer string) (r *ratchet.Ratchet) { 20 | r, err := openRatchet(peer) 21 | if err != nil { 22 | if err == errNoRatchet { 23 | fmt.Fprintf(os.Stderr, "No ratchet for %s, creating one.\n", peer) 24 | r, err = createRatchet(peer) 25 | if err != nil { 26 | log.Fatal("Couldn't create ratchet:", err) 27 | } 28 | } else { 29 | log.Fatal(err) 30 | } 31 | } 32 | return r 33 | 34 | } 35 | 36 | stat, err := os.Stdin.Stat() 37 | if err != nil { 38 | log.Fatal("Couldn't stat stdin") 39 | } 40 | if (stat.Mode() & os.ModeCharDevice) != 0 { 41 | // stdin is from a terminal, not from a pipe 42 | fmt.Fprintln(os.Stderr, "Please paste in the message; when done, hit Ctrl-D\n") 43 | } 44 | stdin, err := ioutil.ReadAll(os.Stdin) 45 | if err != nil { 46 | log.Fatal("Couldn't read from stdin: ", err) 47 | } 48 | 49 | blockScanner := newBlockSplitter(stdin) 50 | var scannedSomething bool 51 | for blockScanner.Scan() { 52 | armorDecoder, err := armor.Decode(strings.NewReader(blockScanner.Text())) 53 | if err != nil { 54 | log.Fatal("Couldn't read message from stdin: ", err) 55 | } 56 | switch armorDecoder.Type { 57 | case ENCRYPTED_MESSAGE_TYPE: 58 | msg, err := ioutil.ReadAll(armorDecoder.Body) 59 | if err != nil { 60 | log.Fatal("Couldn't read message: ", err) 61 | } 62 | r := getRatchet(peer) 63 | plaintext, err := r.Decrypt(msg) 64 | if err != nil { 65 | log.Fatal("Couldn't decrypt message: ", err) 66 | } 67 | fmt.Println("") 68 | io.Copy(os.Stdout, bytes.NewReader(plaintext)) 69 | deleteNew(peer) 70 | scannedSomething = true 71 | case KEY_EXCHANGE_TYPE: 72 | r := getRatchet(peer) 73 | var kx ratchet.KeyExchange 74 | json.NewDecoder(armorDecoder.Body).Decode(&kx) 75 | err = r.CompleteKeyExchange(kx) 76 | if err != nil && err != ratchet.ErrHandshakeComplete { 77 | log.Fatal("Invalid key exchange material: ", err) 78 | } 79 | saveRatchet(r, peer) 80 | scannedSomething = true 81 | default: 82 | log.Println("Unknown block type: ", armorDecoder.Type) 83 | } 84 | } 85 | if err := blockScanner.Err(); err != nil { 86 | log.Fatal("Error scanning blocks: ", err) 87 | } 88 | if !scannedSomething { 89 | fmt.Fprintln(os.Stderr, "The input you provided is invalid") 90 | os.Exit(1) 91 | } 92 | } 93 | 94 | // A blockSplitter is a bufio.Scanner that splits the input into 95 | // multiple armored blocks 96 | type blockSplitter struct { 97 | *bufio.Scanner 98 | } 99 | 100 | func newBlockSplitter(input []byte) blockSplitter { 101 | scanner := bufio.NewScanner(bytes.NewReader(input)) 102 | split := func(data []byte, atEof bool) (advance int, token []byte, err error) { 103 | kxType := fmt.Sprintf("-----END %s-----", KEY_EXCHANGE_TYPE) 104 | kxTypeIdx := bytes.Index(data, []byte(kxType)) 105 | 106 | encryptedType := fmt.Sprintf("-----END %s-----", ENCRYPTED_MESSAGE_TYPE) 107 | encryptedTypeIdx := bytes.Index(data, []byte(encryptedType)) 108 | if kxTypeIdx != -1 { 109 | advance := kxTypeIdx + len(kxType) 110 | return advance, data[:advance], nil 111 | } 112 | if encryptedTypeIdx != -1 { 113 | advance := encryptedTypeIdx + len(encryptedType) 114 | return advance, data[:advance], nil 115 | } 116 | 117 | // No end of armored block, read more 118 | return 0, nil, nil 119 | } 120 | scanner.Split(split) 121 | return blockSplitter{scanner} 122 | } 123 | -------------------------------------------------------------------------------- /pkg/ratchet/ratchet_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2013 Adam Langley. All rights reserved. 2 | // 3 | // Redistribution and use in source and binary forms, with or without 4 | // modification, are permitted provided that the following conditions are 5 | // met: 6 | // 7 | // * Redistributions of source code must retain the above copyright 8 | // notice, this list of conditions and the following disclaimer. 9 | // * Redistributions in binary form must reproduce the above 10 | // copyright notice, this list of conditions and the following disclaimer 11 | // in the documentation and/or other materials provided with the 12 | // distribution. 13 | // * Neither the name Pond nor the names of its contributors may be 14 | // used to endorse or promote products derived from this software without 15 | // specific prior written permission. 16 | // 17 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 18 | // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 19 | // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 20 | // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 21 | // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 22 | // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 23 | // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 24 | // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 25 | // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | 29 | package ratchet 30 | 31 | import ( 32 | "bytes" 33 | "crypto/rand" 34 | "encoding/json" 35 | "io" 36 | "testing" 37 | ) 38 | 39 | func pairedRatchet() (a, b *Ratchet) { 40 | var privA, privB [32]byte 41 | io.ReadFull(rand.Reader, privA[:]) 42 | io.ReadFull(rand.Reader, privB[:]) 43 | 44 | a, b = New(rand.Reader, privA), New(rand.Reader, privB) 45 | 46 | kxA, err := a.GetKeyExchangeMaterial() 47 | if err != nil { 48 | panic(err) 49 | } 50 | kxB, err := b.GetKeyExchangeMaterial() 51 | if err != nil { 52 | panic(err) 53 | } 54 | if err := a.CompleteKeyExchange(kxB); err != nil { 55 | panic(err) 56 | } 57 | if err := b.CompleteKeyExchange(kxA); err != nil { 58 | panic(err) 59 | } 60 | 61 | return 62 | } 63 | 64 | func TestExchange(t *testing.T) { 65 | a, b := pairedRatchet() 66 | 67 | msg := []byte("test message") 68 | encrypted := a.Encrypt(msg) 69 | result, err := b.Decrypt(encrypted) 70 | if err != nil { 71 | t.Fatal(err) 72 | } 73 | if !bytes.Equal(msg, result) { 74 | t.Fatalf("result doesn't match: %x vs %x", msg, result) 75 | } 76 | } 77 | 78 | type scriptAction struct { 79 | // object is one of sendA, sendB or sendDelayed. The first two options 80 | // cause a message to be sent from one party to the other. The latter 81 | // causes a previously delayed message, identified by id, to be 82 | // delivered. 83 | object int 84 | // result is one of deliver, drop or delay. If delay, then the message 85 | // is stored using the value in id. This value can be repeated later 86 | // with a sendDelayed. 87 | result int 88 | id int 89 | } 90 | 91 | const ( 92 | sendA = iota 93 | sendB 94 | sendDelayed 95 | deliver 96 | drop 97 | delay 98 | ) 99 | 100 | func reinitRatchet(t *testing.T, r *Ratchet) *Ratchet { 101 | state, err := json.Marshal(r) 102 | if err != nil { 103 | t.Fatalf("Failed to marshal: %s", err) 104 | } 105 | 106 | newR := New(rand.Reader, r.myIdentityPrivate) 107 | if err := json.Unmarshal(state, newR); err != nil { 108 | t.Fatalf("Failed to unmarshal: %s", err) 109 | } 110 | newR.theirIdentityPublic = r.theirIdentityPublic 111 | 112 | return newR 113 | } 114 | 115 | func testScript(t *testing.T, script []scriptAction) { 116 | type delayedMessage struct { 117 | msg []byte 118 | encrypted []byte 119 | fromA bool 120 | } 121 | delayedMessages := make(map[int]delayedMessage) 122 | a, b := pairedRatchet() 123 | 124 | for i, action := range script { 125 | switch action.object { 126 | case sendA, sendB: 127 | sender, receiver := a, b 128 | if action.object == sendB { 129 | sender, receiver = receiver, sender 130 | } 131 | 132 | var msg [20]byte 133 | rand.Reader.Read(msg[:]) 134 | encrypted := sender.Encrypt(msg[:]) 135 | 136 | switch action.result { 137 | case deliver: 138 | result, err := receiver.Decrypt(encrypted) 139 | if err != nil { 140 | t.Fatalf("#%d: receiver returned error: %s", i, err) 141 | } 142 | if !bytes.Equal(result, msg[:]) { 143 | t.Fatalf("#%d: bad message: got %x, not %x", i, result, msg[:]) 144 | } 145 | case delay: 146 | if _, ok := delayedMessages[action.id]; ok { 147 | t.Fatalf("#%d: already have delayed message with id %d", i, action.id) 148 | } 149 | delayedMessages[action.id] = delayedMessage{msg[:], encrypted, sender == a} 150 | case drop: 151 | } 152 | case sendDelayed: 153 | delayed, ok := delayedMessages[action.id] 154 | if !ok { 155 | t.Fatalf("#%d: no such delayed message id: %d", i, action.id) 156 | } 157 | 158 | receiver := a 159 | if delayed.fromA { 160 | receiver = b 161 | } 162 | 163 | result, err := receiver.Decrypt(delayed.encrypted) 164 | if err != nil { 165 | t.Fatalf("#%d: receiver returned error: %s", i, err) 166 | } 167 | if !bytes.Equal(result, delayed.msg) { 168 | t.Fatalf("#%d: bad message: got %x, not %x", i, result, delayed.msg) 169 | } 170 | } 171 | 172 | a = reinitRatchet(t, a) 173 | b = reinitRatchet(t, b) 174 | } 175 | } 176 | 177 | func TestBackAndForth(t *testing.T) { 178 | testScript(t, []scriptAction{ 179 | {sendA, deliver, -1}, 180 | {sendB, deliver, -1}, 181 | {sendA, deliver, -1}, 182 | {sendB, deliver, -1}, 183 | {sendA, deliver, -1}, 184 | {sendB, deliver, -1}, 185 | }) 186 | } 187 | 188 | func TestReorder(t *testing.T) { 189 | testScript(t, []scriptAction{ 190 | {sendA, deliver, -1}, 191 | {sendA, delay, 0}, 192 | {sendA, deliver, -1}, 193 | {sendDelayed, deliver, 0}, 194 | }) 195 | } 196 | 197 | func TestReorderAfterRatchet(t *testing.T) { 198 | testScript(t, []scriptAction{ 199 | {sendA, deliver, -1}, 200 | {sendA, delay, 0}, 201 | {sendB, deliver, -1}, 202 | {sendA, deliver, -1}, 203 | {sendB, deliver, -1}, 204 | {sendDelayed, deliver, 0}, 205 | }) 206 | } 207 | 208 | func TestDrop(t *testing.T) { 209 | testScript(t, []scriptAction{ 210 | {sendA, drop, -1}, 211 | {sendA, drop, -1}, 212 | {sendA, drop, -1}, 213 | {sendA, drop, -1}, 214 | {sendA, deliver, -1}, 215 | {sendB, deliver, -1}, 216 | }) 217 | } 218 | 219 | func TestMarshal(t *testing.T) { 220 | var priv [32]byte 221 | io.ReadFull(rand.Reader, priv[:]) 222 | ratchet := New(rand.Reader, priv) 223 | kx, err := ratchet.GetKeyExchangeMaterial() 224 | if err != nil { 225 | t.Fatal(err) 226 | } 227 | 228 | marshalled, err := json.Marshal(kx) 229 | if err != nil { 230 | t.Fatal(err) 231 | } 232 | 233 | var kxActual KeyExchange 234 | err = json.Unmarshal(marshalled, &kxActual) 235 | if err != nil { 236 | t.Fatal(err) 237 | } 238 | 239 | if !bytes.Equal(kxActual.Dh[:], kx.Dh[:]) { 240 | t.Fatalf("Dh doesn't match; expected %x, got %x\n", kx.Dh, kxActual.Dh) 241 | } 242 | 243 | if !bytes.Equal(kxActual.Dh1[:], kx.Dh1[:]) { 244 | t.Fatalf("Dh1 doesn't match; expected %x, got %x\n", kx.Dh1, kxActual.Dh1) 245 | } 246 | 247 | if !bytes.Equal(kxActual.IdentityPublic[:], kx.IdentityPublic[:]) { 248 | t.Fatalf("IdentityPublic doesn't match; expected %x, got %x\n", kx.IdentityPublic, kxActual.IdentityPublic) 249 | } 250 | } 251 | 252 | func TestCantDecryptUntilHandshakeComplete(t *testing.T) { 253 | var privA [32]byte 254 | io.ReadFull(rand.Reader, privA[:]) 255 | a := New(rand.Reader, privA) 256 | kx, err := a.GetKeyExchangeMaterial() 257 | if err != nil { 258 | t.Fatal(err) 259 | } 260 | 261 | var privB [32]byte 262 | io.ReadFull(rand.Reader, privB[:]) 263 | b := New(rand.Reader, privB) 264 | b.CompleteKeyExchange(kx) 265 | msg := b.Encrypt([]byte("some message")) 266 | 267 | // a hasn't finished handshake yet, decrypting is not allowed 268 | if _, err := a.Decrypt(msg); err == nil { 269 | t.Fatal("shouldn't be able to decrypt yet") 270 | } 271 | } 272 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Goax 2 | 3 | A pure-go implementation of the Signal protocol as defined in 4 | 5 | https://en.wikipedia.org/wiki/Signal_Protocol 6 | 7 | The actual implementation was written by @agl over here: 8 | 9 | https://github.com/agl/pond/tree/master/client/ratchet 10 | 11 | Any credits go to him. This repo just extracts all pond-specific parts 12 | to make the protocol usable outside of it. 13 | 14 | As such, the usual warning: 15 | 16 | THIS IS PURELY EXPERIMENTAL, DON'T TRUST YOUR COMMUNICATIONS WITH IT 17 | 18 | # What it is 19 | 20 | Goax is an attempt at using the Signal protocol from the command line. 21 | The goal is to have a UX as simple as possible so that making mistakes 22 | is hard. It doesn't do any network communication, that's the part where 23 | you, the user, interact by copy-pasting goax's inputs and outputs 24 | through the medium of your choice. 25 | 26 | # How to use 27 | 28 | goax is an extremely simple attempt at Forward Secure communications on 29 | the command line. All it does is print armor-encoded blocks of text that 30 | you copy-paste into the medium of your choice (email, IM, ...) 31 | 32 | The first thing to do is to build the binary and put it in its own 33 | folder 34 | 35 | ```shell 36 | $ go build 37 | $ cp goax /tmp/comms 38 | $ cd /tmp/comms 39 | ``` 40 | 41 | Once there, it will create some files in the directory. Don't bother 42 | about them. 43 | 44 | Now that it's there you will want to run it, just to see what it does 45 | 46 | ```shell 47 | $ ./goax 48 | Need an action: one of mykey, send or receive 49 | ``` 50 | 51 | Let's see what our key is: 52 | 53 | ```shell 54 | $ ./goax mykey 55 | 2JC2HDtUfBwxMGq4Bkj1DcAiGB2WQ3eByJ9jhyukDMob 56 | ``` 57 | 58 | Of course your key will differ. It is automatically created if it 59 | doesn't exist (it's just a file in the current directory). Run it again; 60 | the key should be the same. 61 | This is your *identity key*. It uniquely identifies this instance of 62 | goax. Delete the `key` file and you have another instance with another 63 | identity; run goax from another directory and you have another instance 64 | with another identity. 65 | 66 | Now that we have an identity, we probably want to send some message to 67 | someone. The first step is to try to send them something. Let's suppose 68 | they are named Barry: 69 | 70 | ```shell 71 | $ ./goax send barry 72 | No ratchet for barry, please send this to the peer and "receive" what they send you back 73 | 74 | -----BEGIN KEY EXCHANGE MATERIAL----- 75 | 76 | eyJpZHB1YiI6IjEzNDMwNWZiNmRlMmU3YTc1NmU5NmRiODI0YmRkYjNkMzA4MTE1 77 | OGRhNzkwZjZiYjUwZGZjZmM3YTI4MDYxNmUiLCJkaCI6ImU1YTYyNDU4ZjJmYWQ2 78 | YTdkNWI2Y2ZkYjI1NWNhZDViNzg4YzVlMDNiNWYxMWY1NWFhYWRjYzk0NTY4Mjc2 79 | MjUiLCJkaDEiOiI1MmFmOGYxNTRiZDk0ZDExNGE1ZjljMWIxMWQxOTMxYTU5Mjhm 80 | Y2MwODY2NDE2NzczZGIwNGRkYmFhNGVmMzBhIn0K 81 | =Xccr 82 | -----END KEY EXCHANGE MATERIAL----- 83 | ``` 84 | 85 | What happened is that since it is the very first time that goax hears 86 | about a "barry", it will create a ratchet (which is some internal state) 87 | for them, and create a *key exchange material*. The details of what it 88 | is aren't important; all you have to do is copy-paste the block 89 | (including the -----BEGIN KEY EXCHANGE MATERIAL----- and 90 | -----END KEY EXCHANGE MATERIAL---- lines) and send it to barry. This 91 | material serves to make a handshake with them; you have to send your 92 | part to them, and they have to send their part to you. 93 | 94 | Note that the argument `barry` is just a string; goax has no idea what 95 | it means. It could very well be an email address (b@rry.com) or a 96 | twitter handle (@rryb) or an ICQ handle. Every "recipient" is considered 97 | a different conversation between your identity key and their identity 98 | key. 99 | 100 | We have sent them the block, and they have done the same on their side 101 | and sent us their block. Here's how we finish the handshake (they should 102 | do the same on their side as well) 103 | 104 | ```shell 105 | $ ./goax receive barry 106 | Please paste in the message; when done, hit Ctrl-D 107 | 108 | -----BEGIN KEY EXCHANGE MATERIAL----- 109 | 110 | eyJpZHB1YiI6IjEzNDMwNWZiNmRlMmU3YTc1NmU5NmRiODI0YmRkYjNkMzA4MTE1 111 | OGRhNzkwZjZiYjUwZGZjZmM3YTI4MDYxNmUiLCJkaCI6Ijc3N2FhNWY0ZmI1NmRl 112 | NTQ4YzI4ZDhmNDIxMjQ3ZDg3YjhkNWI0ZDhiN2I4NjY5NTRlNGZhNTVjOWQ5YjZj 113 | NWMiLCJkaDEiOiJlZTQ4NzYyNDA3NmJmN2JlZmQwYmJiZjkwYWY3MGZjNDlhOGI2 114 | NjA4MmU0Zjg4ODAzZTExZTNkMWQ2YzlhMjIxIn0K 115 | =I2pa 116 | -----END KEY EXCHANGE MATERIAL----- 117 | ^D 118 | $ 119 | ``` 120 | 121 | The command didn't throw anything at us; the handshake is done and we 122 | can now start sending messages ! (Hit Ctrl-D on an empty line when you 123 | are done writing your message) 124 | 125 | ```shell 126 | $ ./goax send barry 127 | 128 | Hello from goax ! 129 | ^D 130 | -----BEGIN GOAX ENCRYPTED MESSAGE----- 131 | 132 | PvEwrQH8CFR9URIRX63SQtBA/BxoAIze/Higmkf54FYswgy5YzYIbiz6/n54dy/o 133 | aLcVeAz2HA+5rReJHeVU5iNNd06BMlPzFxJCp21ssgiiVgSivPHb1/2mv8KMaqK8 134 | cATkOxjryH6xEXmCRWPKTVJVZre2MwgiETdlJkqVwZjcnD7nJogXx2uq 135 | =KNWM 136 | -----END GOAX ENCRYPTED MESSAGE----- 137 | ``` 138 | 139 | This new block type is the actual encrypted message; send that to barry, 140 | and they can read your message: 141 | 142 | ```shell 143 | # From barry's shell 144 | barry$ ./goax receive anon 145 | Please paste in the message; when done, hit Ctrl-D 146 | 147 | -----BEGIN GOAX ENCRYPTED MESSAGE----- 148 | 149 | PvEwrQH8CFR9URIRX63SQtBA/BxoAIze/Higmkf54FYswgy5YzYIbiz6/n54dy/o 150 | aLcVeAz2HA+5rReJHeVU5iNNd06BMlPzFxJCp21ssgiiVgSivPHb1/2mv8KMaqK8 151 | cATkOxjryH6xEXmCRWPKTVJVZre2MwgiETdlJkqVwZjcnD7nJogXx2uq 152 | =KNWM 153 | -----END GOAX ENCRYPTED MESSAGE----- 154 | ^D 155 | 156 | Hello from goax ! 157 | 158 | barry $ 159 | ``` 160 | 161 | Happy communicating ! 162 | 163 | And remember: goax hasn't been audited or analyzed by any competent 164 | cryptographer mind and probably contains multiple issues. Most notably 165 | there is no way for a user to verify the identity of a peer. Don't 166 | expect it to save your life. 167 | 168 | # Alternative flow: sending messages before receiving any of them 169 | 170 | The Signal protocol handshake has been built with asynchronicity in 171 | mind. If you read carefully the previous section, a handshake doesn't 172 | need both peers to be actually discussing, they don't need to be on at 173 | the same time. This means that it is perfectly fine to receive barry's 174 | key exchange material, complete the handshake on our side, and start 175 | sending messages straight away ! 176 | 177 | ```shell 178 | $ ./goax receive barry 179 | Please paste in the message; when done, hit Ctrl-D 180 | 181 | -----BEGIN KEY EXCHANGE MATERIAL----- 182 | 183 | eyJpZHB1YiI6IjEzNDMwNWZiNmRlMmU3YTc1NmU5NmRiODI0YmRkYjNkMzA4MTE1 184 | OGRhNzkwZjZiYjUwZGZjZmM3YTI4MDYxNmUiLCJkaCI6ImY1NjU1MGU4MDYxYWE5 185 | ZmNhM2QzM2UwNzYyMzI1ZWRhNDNhZGU2NDNhYzNlY2M3NWNiNGRkOTg3ZTEyMGFj 186 | NDQiLCJkaDEiOiI0NDEyZDAyMTFhNDNiNDY5NjhlMTQ1MDQxZWZkZWY2ZDAyM2Jj 187 | ZWM1ODAxMjA5NzFlMjc3ZWU1ODU3MmJjZTJiIn0K 188 | =ossT 189 | -----END KEY EXCHANGE MATERIAL----- 190 | No ratchet for barry, creating one. 191 | 192 | ``` 193 | 194 | In this particular case, we didn't even know that barry existed; maybe 195 | they wanted to talk to us ? In any case, the handshake is complete *on 196 | our side*, so we can start sending messages straight away: 197 | 198 | ```shell 199 | ./goax send barry 200 | 201 | Happy to hear from you ! 202 | ^D 203 | -----BEGIN KEY EXCHANGE MATERIAL----- 204 | 205 | eyJpZHB1YiI6IjEzNDMwNWZiNmRlMmU3YTc1NmU5NmRiODI0YmRkYjNkMzA4MTE1 206 | OGRhNzkwZjZiYjUwZGZjZmM3YTI4MDYxNmUiLCJkaCI6ImFmZTY1YjRhYWQzNzkw 207 | OWY0MzQ3MGE3NDFlN2UyOWY1YjBlM2Q1NjViMjExZGUwNzVmODJiZGZiNTAwZTU4 208 | NWMiLCJkaDEiOiJlODE0NTNkMDZlZWYxOTE1MjlkMGM1ZjcxYjgwNDRjYzA0NTc4 209 | ZjliM2U5MjBkZWMzMDhiN2RkM2JkYzdlNDI2In0K 210 | =QhHJ 211 | -----END KEY EXCHANGE MATERIAL----- 212 | -----BEGIN GOAX ENCRYPTED MESSAGE----- 213 | 214 | ERXsufvBbGhICLZt5CBXrTZsRIKvVt0TKTRi05erz6aRkgIS/a9OYzvUm5wk40To 215 | Payo6OYQOx8b1Hkqee4iNCzeiE+tEMVXBZEtI+AuZtNUIolRDtpI9vudH2XgYgkR 216 | 4hr9WnYbtCB1mOMvqHh0OA6LKd2aV3QDzmbC/7BknzBCEUTHLMA0jzGuV410j0VU 217 | NA== 218 | =w59U 219 | -----END GOAX ENCRYPTED MESSAGE----- 220 | 221 | $ 222 | ``` 223 | 224 | Since we're not sure that barry has finished the handshake on their 225 | side, it is safe to send them key exchange material again. Just copy 226 | paste the 2 blocks and send them; barry will `receive` all of it and 227 | goax will make what is necessary. 228 | 229 | At a later time, when barry sends us a message and we successfully 230 | decrypt it, we have 100% assurance that they have finished the handshake 231 | on their side; goax won't output the key exchange material anymore. 232 | -------------------------------------------------------------------------------- /pkg/ratchet/ratchet.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2013 Adam Langley. All rights reserved. 2 | // 3 | // Redistribution and use in source and binary forms, with or without 4 | // modification, are permitted provided that the following conditions are 5 | // met: 6 | // 7 | // * Redistributions of source code must retain the above copyright 8 | // notice, this list of conditions and the following disclaimer. 9 | // * Redistributions in binary form must reproduce the above 10 | // copyright notice, this list of conditions and the following disclaimer 11 | // in the documentation and/or other materials provided with the 12 | // distribution. 13 | // * Neither the name Pond nor the names of its contributors may be 14 | // used to endorse or promote products derived from this software without 15 | // specific prior written permission. 16 | // 17 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 18 | // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 19 | // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 20 | // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 21 | // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 22 | // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 23 | // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 24 | // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 25 | // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | 29 | // Package ratchet implements the axolotl ratchet, by Trevor Perrin. See 30 | // https://github.com/trevp/axolotl/wiki. 31 | package ratchet 32 | 33 | import ( 34 | "bytes" 35 | "crypto/hmac" 36 | "crypto/sha256" 37 | "encoding/binary" 38 | "encoding/hex" 39 | "encoding/json" 40 | "errors" 41 | "hash" 42 | "io" 43 | "time" 44 | 45 | "golang.org/x/crypto/curve25519" 46 | "golang.org/x/crypto/nacl/secretbox" 47 | ) 48 | 49 | const ( 50 | // headerSize is the size, in bytes, of a header's plaintext contents. 51 | headerSize = 4 /* uint32 message count */ + 52 | 4 /* uint32 previous message count */ + 53 | 32 /* curve25519 ratchet public */ + 54 | 24 /* nonce for message */ 55 | // sealedHeader is the size, in bytes, of an encrypted header. 56 | sealedHeaderSize = 24 /* nonce */ + headerSize + secretbox.Overhead 57 | // nonceInHeaderOffset is the offset of the message nonce in the 58 | // header's plaintext. 59 | nonceInHeaderOffset = 4 + 4 + 32 60 | // maxMissingMessages is the maximum number of missing messages that 61 | // we'll keep track of. 62 | maxMissingMessages = 8 63 | ) 64 | 65 | type KeyExchange struct { 66 | IdentityPublic [32]byte `bencode:"identity"` 67 | Dh [32]byte `bencode:"dh"` 68 | Dh1 [32]byte `bencode:"dh1"` 69 | } 70 | 71 | // MarshalJSON makes the KeyExchange a json.Marshaler by hex-ing fields 72 | // before putting them in the json 73 | func (k KeyExchange) MarshalJSON() ([]byte, error) { 74 | hexified := struct { 75 | IdentityPublic string `json:"idpub"` 76 | Dh string `json:"dh"` 77 | Dh1 string `json:"dh1"` 78 | }{ 79 | IdentityPublic: hex.EncodeToString(k.IdentityPublic[:]), 80 | Dh: hex.EncodeToString(k.Dh[:]), 81 | Dh1: hex.EncodeToString(k.Dh1[:]), 82 | } 83 | 84 | return json.Marshal(hexified) 85 | } 86 | 87 | // UnmarshalJSON makes the *KeyExchange a json.Unmarshaler by un-hex-ing 88 | // fields after taking them from the json 89 | func (k *KeyExchange) UnmarshalJSON(in []byte) error { 90 | type hexified struct { 91 | IdentityPublic string `json:"idpub"` 92 | Dh string `json:"dh"` 93 | Dh1 string `json:"dh1"` 94 | } 95 | var h hexified 96 | err := json.Unmarshal(in, &h) 97 | if err != nil { 98 | return err 99 | } 100 | 101 | idpub, err := hex.DecodeString(h.IdentityPublic) 102 | if err != nil { 103 | return err 104 | } 105 | dh, err := hex.DecodeString(h.Dh) 106 | if err != nil { 107 | return err 108 | } 109 | dh1, err := hex.DecodeString(h.Dh1) 110 | if err != nil { 111 | return err 112 | } 113 | 114 | copy(k.IdentityPublic[:], idpub) 115 | copy(k.Dh[:], dh) 116 | copy(k.Dh1[:], dh1) 117 | 118 | return nil 119 | } 120 | 121 | // Ratchet contains the per-contact, crypto state. 122 | type Ratchet struct { 123 | // myIdentityPrivate and TheirIdentityPublic contain the primary, 124 | // curve25519 identity keys. 125 | myIdentityPrivate, theirIdentityPublic [32]byte 126 | 127 | // rootKey gets updated by the DH ratchet. 128 | rootKey [32]byte 129 | // Header keys are used to encrypt message headers. 130 | sendHeaderKey, recvHeaderKey [32]byte 131 | nextSendHeaderKey, nextRecvHeaderKey [32]byte 132 | // Chain keys are used for forward secrecy updating. 133 | sendChainKey, recvChainKey [32]byte 134 | sendRatchetPrivate, recvRatchetPublic [32]byte 135 | sendCount, recvCount uint32 136 | prevSendCount uint32 137 | // ratchet is true if we will send a new ratchet value in the next message. 138 | ratchet bool 139 | 140 | // saved is a map from a header key to a map from sequence number to 141 | // message key. 142 | saved map[[32]byte]map[uint32]savedKey 143 | 144 | // kxPrivate0 and kxPrivate1 contain curve25519 private values during 145 | // the key exchange phase. 146 | kxPrivate0, kxPrivate1 *[32]byte 147 | 148 | // isHandshakeComplete tells if the key exchange was completed in 149 | // both directions 150 | isHandshakeComplete bool 151 | 152 | rand io.Reader 153 | } 154 | 155 | // MyPriv returns the hex-encoded private DH key 156 | func (r *Ratchet) MyPriv() string { 157 | return hex.EncodeToString(r.myIdentityPrivate[:]) 158 | } 159 | 160 | // savedKey contains a message key and timestamp for a message which has not 161 | // been received. The timestamp comes from the message by which we learn of the 162 | // missing message. 163 | type savedKey struct { 164 | key [32]byte 165 | timestamp time.Time 166 | } 167 | 168 | func (r *Ratchet) randBytes(buf []byte) { 169 | if _, err := io.ReadFull(r.rand, buf); err != nil { 170 | panic(err) 171 | } 172 | } 173 | 174 | func New(rand io.Reader, myPriv [32]byte) *Ratchet { 175 | r := &Ratchet{ 176 | rand: rand, 177 | kxPrivate0: new([32]byte), 178 | kxPrivate1: new([32]byte), 179 | saved: make(map[[32]byte]map[uint32]savedKey), 180 | myIdentityPrivate: myPriv, 181 | } 182 | 183 | r.randBytes(r.kxPrivate0[:]) 184 | r.randBytes(r.kxPrivate1[:]) 185 | 186 | return r 187 | } 188 | 189 | // GetKeyExchangeMaterial returns key exchange information from the 190 | // ratchet. 191 | func (r *Ratchet) GetKeyExchangeMaterial() (kx KeyExchange, err error) { 192 | 193 | var public0, public1, myIdentity [32]byte 194 | curve25519.ScalarBaseMult(&public0, r.kxPrivate0) 195 | curve25519.ScalarBaseMult(&public1, r.kxPrivate1) 196 | curve25519.ScalarBaseMult(&myIdentity, &r.myIdentityPrivate) 197 | 198 | kx = KeyExchange{ 199 | IdentityPublic: myIdentity, 200 | Dh: public0, 201 | Dh1: public1, 202 | } 203 | 204 | return 205 | } 206 | 207 | // deriveKey takes an HMAC object and a label and calculates out = HMAC(k, label). 208 | func deriveKey(out *[32]byte, label []byte, h hash.Hash) { 209 | h.Reset() 210 | h.Write(label) 211 | n := h.Sum(out[:0]) 212 | if &n[0] != &out[0] { 213 | panic("hash function too large") 214 | } 215 | } 216 | 217 | // These constants are used as the label argument to deriveKey to derive 218 | // independent keys from a master key. 219 | var ( 220 | chainKeyLabel = []byte("chain key") 221 | headerKeyLabel = []byte("header key") 222 | nextRecvHeaderKeyLabel = []byte("next receive header key") 223 | rootKeyLabel = []byte("root key") 224 | rootKeyUpdateLabel = []byte("root key update") 225 | sendHeaderKeyLabel = []byte("next send header key") 226 | messageKeyLabel = []byte("message key") 227 | chainKeyStepLabel = []byte("chain key step") 228 | ) 229 | 230 | var ErrHandshakeComplete = errors.New("ratchet: handshake already complete") 231 | var ErrHandshakeNotComplete = errors.New("ratchet: handshake not complete yet") 232 | 233 | // CompleteKeyExchange takes a KeyExchange message from the other party and 234 | // establishes the ratchet. 235 | func (r *Ratchet) CompleteKeyExchange(kx KeyExchange) error { 236 | if r.isHandshakeComplete { 237 | return ErrHandshakeComplete 238 | } 239 | 240 | var public0 [32]byte 241 | curve25519.ScalarBaseMult(&public0, r.kxPrivate0) 242 | 243 | if len(kx.Dh) != len(public0) { 244 | return errors.New("ratchet: peer's key exchange is invalid") 245 | } 246 | if len(kx.Dh1) != len(public0) { 247 | return errors.New("ratchet: peer using old-form key exchange") 248 | } 249 | 250 | var myIdentity [32]byte 251 | curve25519.ScalarBaseMult(&myIdentity, &r.myIdentityPrivate) 252 | 253 | if len(kx.IdentityPublic) != len(myIdentity) { 254 | return errors.New("Invalid identity length") 255 | } 256 | copy(r.theirIdentityPublic[:], kx.IdentityPublic[:]) 257 | 258 | var amAlice bool 259 | switch bytes.Compare(public0[:], []byte(kx.Dh[:])) { 260 | case -1: 261 | amAlice = true 262 | case 1: 263 | amAlice = false 264 | case 0: 265 | return errors.New("ratchet: peer echoed our own DH values back") 266 | } 267 | 268 | var theirDH [32]byte 269 | copy(theirDH[:], kx.Dh[:]) 270 | 271 | keyMaterial := make([]byte, 0, 32*5) 272 | var sharedKey [32]byte 273 | curve25519.ScalarMult(&sharedKey, r.kxPrivate0, &theirDH) 274 | keyMaterial = append(keyMaterial, sharedKey[:]...) 275 | 276 | if amAlice { 277 | curve25519.ScalarMult(&sharedKey, &r.myIdentityPrivate, &theirDH) 278 | keyMaterial = append(keyMaterial, sharedKey[:]...) 279 | curve25519.ScalarMult(&sharedKey, r.kxPrivate0, &r.theirIdentityPublic) 280 | keyMaterial = append(keyMaterial, sharedKey[:]...) 281 | } else { 282 | curve25519.ScalarMult(&sharedKey, r.kxPrivate0, &r.theirIdentityPublic) 283 | keyMaterial = append(keyMaterial, sharedKey[:]...) 284 | curve25519.ScalarMult(&sharedKey, &r.myIdentityPrivate, &theirDH) 285 | keyMaterial = append(keyMaterial, sharedKey[:]...) 286 | } 287 | 288 | h := hmac.New(sha256.New, keyMaterial) 289 | deriveKey(&r.rootKey, rootKeyLabel, h) 290 | if amAlice { 291 | deriveKey(&r.recvHeaderKey, headerKeyLabel, h) 292 | deriveKey(&r.nextSendHeaderKey, sendHeaderKeyLabel, h) 293 | deriveKey(&r.nextRecvHeaderKey, nextRecvHeaderKeyLabel, h) 294 | deriveKey(&r.recvChainKey, chainKeyLabel, h) 295 | copy(r.recvRatchetPublic[:], kx.Dh1[:]) 296 | } else { 297 | deriveKey(&r.sendHeaderKey, headerKeyLabel, h) 298 | deriveKey(&r.nextRecvHeaderKey, sendHeaderKeyLabel, h) 299 | deriveKey(&r.nextSendHeaderKey, nextRecvHeaderKeyLabel, h) 300 | deriveKey(&r.sendChainKey, chainKeyLabel, h) 301 | copy(r.sendRatchetPrivate[:], r.kxPrivate1[:]) 302 | } 303 | 304 | r.ratchet = amAlice 305 | r.isHandshakeComplete = true 306 | 307 | return nil 308 | } 309 | 310 | // Encrypt acts like append() but appends an encrypted version of msg to 311 | // out. If the handshake is not complete yet, the error will be 312 | // ErrHandshakeNotComplete; otherwise it is nil. 313 | func (r *Ratchet) Encrypt(msg []byte) ([]byte, error) { 314 | if !r.isHandshakeComplete { 315 | return nil, ErrHandshakeNotComplete 316 | } 317 | 318 | if r.ratchet { 319 | r.randBytes(r.sendRatchetPrivate[:]) 320 | copy(r.sendHeaderKey[:], r.nextSendHeaderKey[:]) 321 | 322 | var sharedKey, keyMaterial [32]byte 323 | curve25519.ScalarMult(&sharedKey, &r.sendRatchetPrivate, &r.recvRatchetPublic) 324 | sha := sha256.New() 325 | sha.Write(rootKeyUpdateLabel) 326 | sha.Write(r.rootKey[:]) 327 | sha.Write(sharedKey[:]) 328 | 329 | sha.Sum(keyMaterial[:0]) 330 | h := hmac.New(sha256.New, keyMaterial[:]) 331 | deriveKey(&r.rootKey, rootKeyLabel, h) 332 | deriveKey(&r.nextSendHeaderKey, sendHeaderKeyLabel, h) 333 | deriveKey(&r.sendChainKey, chainKeyLabel, h) 334 | r.prevSendCount, r.sendCount = r.sendCount, 0 335 | r.ratchet = false 336 | } 337 | 338 | h := hmac.New(sha256.New, r.sendChainKey[:]) 339 | var messageKey [32]byte 340 | deriveKey(&messageKey, messageKeyLabel, h) 341 | deriveKey(&r.sendChainKey, chainKeyStepLabel, h) 342 | 343 | var sendRatchetPublic [32]byte 344 | curve25519.ScalarBaseMult(&sendRatchetPublic, &r.sendRatchetPrivate) 345 | var header [headerSize]byte 346 | var headerNonce, messageNonce [24]byte 347 | r.randBytes(headerNonce[:]) 348 | r.randBytes(messageNonce[:]) 349 | 350 | binary.LittleEndian.PutUint32(header[0:4], r.sendCount) 351 | binary.LittleEndian.PutUint32(header[4:8], r.prevSendCount) 352 | copy(header[8:], sendRatchetPublic[:]) 353 | copy(header[nonceInHeaderOffset:], messageNonce[:]) 354 | out := make([]byte, len(headerNonce)) 355 | copy(out, headerNonce[:]) 356 | out = secretbox.Seal(out, header[:], &headerNonce, &r.sendHeaderKey) 357 | r.sendCount++ 358 | return secretbox.Seal(out, msg, &messageNonce, &messageKey), nil 359 | } 360 | 361 | // trySavedKeys tries to decrypt ciphertext using keys saved for missing messages. 362 | func (r *Ratchet) trySavedKeys(ciphertext []byte) ([]byte, error) { 363 | if len(ciphertext) < sealedHeaderSize { 364 | return nil, errors.New("ratchet: header too small to be valid") 365 | } 366 | 367 | sealedHeader := ciphertext[:sealedHeaderSize] 368 | var nonce [24]byte 369 | copy(nonce[:], sealedHeader) 370 | sealedHeader = sealedHeader[len(nonce):] 371 | 372 | for headerKey, messageKeys := range r.saved { 373 | header, ok := secretbox.Open(nil, sealedHeader, &nonce, &headerKey) 374 | if !ok { 375 | continue 376 | } 377 | if len(header) != headerSize { 378 | continue 379 | } 380 | msgNum := binary.LittleEndian.Uint32(header[:4]) 381 | msgKey, ok := messageKeys[msgNum] 382 | if !ok { 383 | // This is a fairly common case: the message key might 384 | // not have been saved because it's the next message 385 | // key. 386 | return nil, nil 387 | } 388 | 389 | sealedMessage := ciphertext[sealedHeaderSize:] 390 | copy(nonce[:], header[nonceInHeaderOffset:]) 391 | msg, ok := secretbox.Open(nil, sealedMessage, &nonce, &msgKey.key) 392 | if !ok { 393 | return nil, errors.New("ratchet: corrupt message") 394 | } 395 | delete(messageKeys, msgNum) 396 | if len(messageKeys) == 0 { 397 | delete(r.saved, headerKey) 398 | } 399 | return msg, nil 400 | } 401 | 402 | return nil, nil 403 | } 404 | 405 | // saveKeys takes a header key, the current chain key, a received message 406 | // number and the expected message number and advances the chain key as needed. 407 | // It returns the message key for given given message number and the new chain 408 | // key. If any messages have been skipped over, it also returns savedKeys, a 409 | // map suitable for merging with r.saved, that contains the message keys for 410 | // the missing messages. 411 | func (r *Ratchet) saveKeys(headerKey, recvChainKey *[32]byte, messageNum, receivedCount uint32) (provisionalChainKey, messageKey [32]byte, savedKeys map[[32]byte]map[uint32]savedKey, err error) { 412 | if messageNum < receivedCount { 413 | // This is a message from the past, but we didn't have a saved 414 | // key for it, which means that it's a duplicate message or we 415 | // expired the save key. 416 | err = errors.New("ratchet: duplicate message or message delayed longer than tolerance") 417 | return 418 | } 419 | 420 | missingMessages := messageNum - receivedCount 421 | if missingMessages > maxMissingMessages { 422 | err = errors.New("ratchet: message exceeds reordering limit") 423 | return 424 | } 425 | 426 | // messageKeys maps from message number to message key. 427 | var messageKeys map[uint32]savedKey 428 | now := time.Now() 429 | if missingMessages > 0 { 430 | messageKeys = make(map[uint32]savedKey) 431 | } 432 | 433 | copy(provisionalChainKey[:], recvChainKey[:]) 434 | 435 | for n := receivedCount; n <= messageNum; n++ { 436 | h := hmac.New(sha256.New, provisionalChainKey[:]) 437 | deriveKey(&messageKey, messageKeyLabel, h) 438 | deriveKey(&provisionalChainKey, chainKeyStepLabel, h) 439 | if n < messageNum { 440 | messageKeys[n] = savedKey{messageKey, now} 441 | } 442 | } 443 | 444 | if messageKeys != nil { 445 | savedKeys = make(map[[32]byte]map[uint32]savedKey) 446 | savedKeys[*headerKey] = messageKeys 447 | } 448 | 449 | return 450 | } 451 | 452 | // mergeSavedKeys takes a map of saved message keys from saveKeys and merges it 453 | // into r.saved. 454 | func (r *Ratchet) mergeSavedKeys(newKeys map[[32]byte]map[uint32]savedKey) { 455 | for headerKey, newMessageKeys := range newKeys { 456 | messageKeys, ok := r.saved[headerKey] 457 | if !ok { 458 | r.saved[headerKey] = newMessageKeys 459 | continue 460 | } 461 | 462 | for n, messageKey := range newMessageKeys { 463 | messageKeys[n] = messageKey 464 | } 465 | } 466 | } 467 | 468 | // isZeroKey returns true if key is all zeros. 469 | func isZeroKey(key *[32]byte) bool { 470 | var x uint8 471 | for _, v := range key { 472 | x |= v 473 | } 474 | 475 | return x == 0 476 | } 477 | 478 | func (r *Ratchet) Decrypt(ciphertext []byte) ([]byte, error) { 479 | if !r.isHandshakeComplete { 480 | return nil, ErrHandshakeNotComplete 481 | } 482 | 483 | msg, err := r.trySavedKeys(ciphertext) 484 | if err != nil || msg != nil { 485 | return msg, err 486 | } 487 | 488 | sealedHeader := ciphertext[:sealedHeaderSize] 489 | sealedMessage := ciphertext[sealedHeaderSize:] 490 | var nonce [24]byte 491 | copy(nonce[:], sealedHeader) 492 | sealedHeader = sealedHeader[len(nonce):] 493 | 494 | header, ok := secretbox.Open(nil, sealedHeader, &nonce, &r.recvHeaderKey) 495 | ok = ok && !isZeroKey(&r.recvHeaderKey) 496 | if ok { 497 | if len(header) != headerSize { 498 | return nil, errors.New("ratchet: incorrect header size") 499 | } 500 | messageNum := binary.LittleEndian.Uint32(header[:4]) 501 | provisionalChainKey, messageKey, savedKeys, err := r.saveKeys(&r.recvHeaderKey, &r.recvChainKey, messageNum, r.recvCount) 502 | if err != nil { 503 | return nil, err 504 | } 505 | 506 | copy(nonce[:], header[nonceInHeaderOffset:]) 507 | msg, ok := secretbox.Open(nil, sealedMessage, &nonce, &messageKey) 508 | if !ok { 509 | return nil, errors.New("ratchet: corrupt message") 510 | } 511 | 512 | copy(r.recvChainKey[:], provisionalChainKey[:]) 513 | r.mergeSavedKeys(savedKeys) 514 | r.recvCount = messageNum + 1 515 | return msg, nil 516 | } 517 | 518 | header, ok = secretbox.Open(nil, sealedHeader, &nonce, &r.nextRecvHeaderKey) 519 | if !ok { 520 | return nil, errors.New("ratchet: cannot decrypt") 521 | } 522 | if len(header) != headerSize { 523 | return nil, errors.New("ratchet: incorrect header size") 524 | } 525 | 526 | if r.ratchet { 527 | return nil, errors.New("ratchet: received message encrypted to next header key without ratchet flag set") 528 | } 529 | 530 | messageNum := binary.LittleEndian.Uint32(header[:4]) 531 | prevMessageCount := binary.LittleEndian.Uint32(header[4:8]) 532 | 533 | _, _, oldSavedKeys, err := r.saveKeys(&r.recvHeaderKey, &r.recvChainKey, prevMessageCount, r.recvCount) 534 | if err != nil { 535 | return nil, err 536 | } 537 | 538 | var dhPublic, sharedKey, rootKey, chainKey, keyMaterial [32]byte 539 | copy(dhPublic[:], header[8:]) 540 | 541 | curve25519.ScalarMult(&sharedKey, &r.sendRatchetPrivate, &dhPublic) 542 | 543 | sha := sha256.New() 544 | sha.Write(rootKeyUpdateLabel) 545 | sha.Write(r.rootKey[:]) 546 | sha.Write(sharedKey[:]) 547 | 548 | var rootKeyHMAC hash.Hash 549 | 550 | sha.Sum(keyMaterial[:0]) 551 | rootKeyHMAC = hmac.New(sha256.New, keyMaterial[:]) 552 | deriveKey(&rootKey, rootKeyLabel, rootKeyHMAC) 553 | deriveKey(&chainKey, chainKeyLabel, rootKeyHMAC) 554 | 555 | provisionalChainKey, messageKey, savedKeys, err := r.saveKeys(&r.nextRecvHeaderKey, &chainKey, messageNum, 0) 556 | if err != nil { 557 | return nil, err 558 | } 559 | 560 | copy(nonce[:], header[nonceInHeaderOffset:]) 561 | msg, ok = secretbox.Open(nil, sealedMessage, &nonce, &messageKey) 562 | if !ok { 563 | return nil, errors.New("ratchet: corrupt message") 564 | } 565 | 566 | copy(r.rootKey[:], rootKey[:]) 567 | copy(r.recvChainKey[:], provisionalChainKey[:]) 568 | copy(r.recvHeaderKey[:], r.nextRecvHeaderKey[:]) 569 | deriveKey(&r.nextRecvHeaderKey, sendHeaderKeyLabel, rootKeyHMAC) 570 | for i := range r.sendRatchetPrivate { 571 | r.sendRatchetPrivate[i] = 0 572 | } 573 | copy(r.recvRatchetPublic[:], dhPublic[:]) 574 | 575 | r.recvCount = messageNum + 1 576 | r.mergeSavedKeys(oldSavedKeys) 577 | r.mergeSavedKeys(savedKeys) 578 | r.ratchet = true 579 | 580 | return msg, nil 581 | } 582 | 583 | func dup(key *[32]byte) []byte { 584 | if key == nil { 585 | return nil 586 | } 587 | 588 | ret := make([]byte, 32) 589 | copy(ret, key[:]) 590 | return ret 591 | } 592 | 593 | type ratchetState struct { 594 | RootKey []byte `json:"root_key,omitempty"` 595 | SendHeaderKey []byte `json:"send_header_key,omitempty"` 596 | RecvHeaderKey []byte `json:"recv_header_key,omitempty"` 597 | NextSendHeaderKey []byte `json:"next_send_header_key,omitempty"` 598 | NextRecvHeaderKey []byte `json:"next_recv_header_key,omitempty"` 599 | SendChainKey []byte `json:"send_chain_key,omitempty"` 600 | RecvChainKey []byte `json:"recv_chain_key,omitempty"` 601 | SendRatchetPrivate []byte `json:"send_ratchet_private,omitempty"` 602 | RecvRatchetPublic []byte `json:"recv_ratchet_public,omitempty"` 603 | SendCount uint32 `json:"send_count,omitempty"` 604 | RecvCount uint32 `json:"recv_count,omitempty"` 605 | PrevSendCount uint32 `json:"prev_send_count,omitempty"` 606 | Ratchet bool `json:"ratchet,omitempty"` 607 | V2 bool `json:"v2,omitempty"` 608 | Private0 []byte `json:"private0,omitempty"` 609 | Private1 []byte `json:"private1,omitempty"` 610 | IsHandshakeComplete bool `json:"isHandshakeComplete,omitempty"` 611 | SavedKeys []ratchetState_SavedKeys `json:"saved_keys,omitempty"` 612 | XXX_unrecognized []byte `json:"-"` 613 | } 614 | 615 | type ratchetState_SavedKeys struct { 616 | HeaderKey []byte `json:"header_key,omitempty"` 617 | MessageKeys []ratchetState_SavedKeys_MessageKey `json:"message_keys,omitempty"` 618 | XXX_unrecognized []byte `json:"-"` 619 | } 620 | 621 | type ratchetState_SavedKeys_MessageKey struct { 622 | Num uint32 `json:"num,omitempty"` 623 | Key []byte `json:"key,omitempty"` 624 | CreationTime int64 `json:"creation_time,omitempty"` 625 | XXX_unrecognized []byte `json:"-"` 626 | } 627 | 628 | func (r *Ratchet) MarshalJSON() ([]byte, error) { 629 | s := ratchetState{ 630 | RootKey: dup(&r.rootKey), 631 | SendHeaderKey: dup(&r.sendHeaderKey), 632 | RecvHeaderKey: dup(&r.recvHeaderKey), 633 | NextSendHeaderKey: dup(&r.nextSendHeaderKey), 634 | NextRecvHeaderKey: dup(&r.nextRecvHeaderKey), 635 | SendChainKey: dup(&r.sendChainKey), 636 | RecvChainKey: dup(&r.recvChainKey), 637 | SendRatchetPrivate: dup(&r.sendRatchetPrivate), 638 | RecvRatchetPublic: dup(&r.recvRatchetPublic), 639 | SendCount: r.sendCount, 640 | RecvCount: r.recvCount, 641 | PrevSendCount: r.prevSendCount, 642 | Ratchet: r.ratchet, 643 | Private0: dup(r.kxPrivate0), 644 | Private1: dup(r.kxPrivate1), 645 | IsHandshakeComplete: r.isHandshakeComplete, 646 | } 647 | 648 | for headerKey, messageKeys := range r.saved { 649 | keys := make([]ratchetState_SavedKeys_MessageKey, 0, len(messageKeys)) 650 | for messageNum, savedKey := range messageKeys { 651 | keys = append(keys, ratchetState_SavedKeys_MessageKey{ 652 | Num: messageNum, 653 | Key: dup(&savedKey.key), 654 | CreationTime: savedKey.timestamp.Unix(), 655 | }) 656 | } 657 | s.SavedKeys = append(s.SavedKeys, ratchetState_SavedKeys{ 658 | HeaderKey: dup(&headerKey), 659 | MessageKeys: keys, 660 | }) 661 | } 662 | 663 | return json.Marshal(s) 664 | } 665 | 666 | func unmarshalKey(dst *[32]byte, src []byte) bool { 667 | if len(src) != 32 { 668 | return false 669 | } 670 | copy(dst[:], src) 671 | return true 672 | } 673 | 674 | var badSerialisedKeyLengthErr = errors.New("ratchet: bad serialised key length") 675 | 676 | func (r *Ratchet) UnmarshalJSON(in []byte) error { 677 | var s ratchetState 678 | err := json.Unmarshal(in, &s) 679 | if err != nil { 680 | return err 681 | } 682 | 683 | if !unmarshalKey(&r.rootKey, s.RootKey) || 684 | !unmarshalKey(&r.sendHeaderKey, s.SendHeaderKey) || 685 | !unmarshalKey(&r.recvHeaderKey, s.RecvHeaderKey) || 686 | !unmarshalKey(&r.nextSendHeaderKey, s.NextSendHeaderKey) || 687 | !unmarshalKey(&r.nextRecvHeaderKey, s.NextRecvHeaderKey) || 688 | !unmarshalKey(&r.sendChainKey, s.SendChainKey) || 689 | !unmarshalKey(&r.recvChainKey, s.RecvChainKey) || 690 | !unmarshalKey(&r.sendRatchetPrivate, s.SendRatchetPrivate) || 691 | !unmarshalKey(&r.recvRatchetPublic, s.RecvRatchetPublic) { 692 | return badSerialisedKeyLengthErr 693 | } 694 | 695 | r.sendCount = s.SendCount 696 | r.recvCount = s.RecvCount 697 | r.prevSendCount = s.PrevSendCount 698 | r.ratchet = s.Ratchet 699 | r.isHandshakeComplete = s.IsHandshakeComplete 700 | 701 | if len(s.Private0) > 0 { 702 | if !unmarshalKey(r.kxPrivate0, s.Private0) || 703 | !unmarshalKey(r.kxPrivate1, s.Private1) { 704 | return badSerialisedKeyLengthErr 705 | } 706 | } else { 707 | r.kxPrivate0 = nil 708 | r.kxPrivate1 = nil 709 | } 710 | 711 | for _, saved := range s.SavedKeys { 712 | var headerKey [32]byte 713 | if !unmarshalKey(&headerKey, saved.HeaderKey) { 714 | return badSerialisedKeyLengthErr 715 | } 716 | messageKeys := make(map[uint32]savedKey) 717 | for _, messageKey := range saved.MessageKeys { 718 | var savedKey savedKey 719 | if !unmarshalKey(&savedKey.key, messageKey.Key) { 720 | return badSerialisedKeyLengthErr 721 | } 722 | savedKey.timestamp = time.Unix(messageKey.CreationTime, 0) 723 | messageKeys[messageKey.Num] = savedKey 724 | } 725 | 726 | r.saved[headerKey] = messageKeys 727 | } 728 | 729 | return nil 730 | } 731 | --------------------------------------------------------------------------------