Skip to content

Instantly share code, notes, and snippets.

@juniorz
Last active February 11, 2018 08:26
Show Gist options
  • Save juniorz/9ae1d1f38fc24a2cb051 to your computer and use it in GitHub Desktop.
Save juniorz/9ae1d1f38fc24a2cb051 to your computer and use it in GitHub Desktop.
An OTR-over-TCP example

# OTR-over-TCP chat-roulette

This is an example of how OTR can be used outside of XMPP

Installing instructions

How to start a server

curl https://gist.github.com/juniorz/9ae1d1f38fc24a2cb051/raw/server.go -O
go run server.go

The server was copied from inspired by Andrew Gerrand's TCP chat roulette example (http://talks.golang.org/2012/chat.slide)

Using a plain-TCP client

telnet localhost 4000

Using an OTR client

curl https://gist.github.com/juniorz/9ae1d1f38fc24a2cb051/raw/client.go -O
go run client.go

How to play

Try out different combinations of plain-TCP and OTR clients.

Things to understand about OTR

You can see by yourself that...

  • OTR is an opporunistic protocol: it doesn't require any OTR-supported server
  • OTR play nicely with others: it doesn't break the communication with non-OTR
  • OTR can auto-start the channel without user intervention

And you should also remember that...

  • OTR requires a Private Key
  • You must verify each other key's fingerprint

Troubleshooting

If something does not work well, update to the latest otr3 by running:

go get -u github.com/twstrike/otr3

package main
import (
"bufio"
"crypto/rand"
"encoding/hex"
"fmt"
"io"
"log"
"net"
"os"
"github.com/twstrike/otr3"
)
const (
EOM = '\n'
)
func main() {
conn, err := net.Dial("tcp", "localhost:4000")
if err != nil {
panic(err)
}
errc := make(chan error, 1)
peer := newPeer(conn)
fmt.Println("Your fingerprint:", peer.fingerprint())
go send(peer, errc)
go receive(peer, errc)
err = <-errc
log.Println(err)
conn.Close()
}
type peer struct {
otr *otr3.Conversation
privKey *otr3.PrivateKey
rw io.ReadWriteCloser
}
func newPeer(rw io.ReadWriteCloser) *peer {
c := &otr3.Conversation{}
priv := &otr3.PrivateKey{}
priv.Generate(rand.Reader)
c.SetKeys(priv, nil)
c.Policies.AllowV3()
//c.Policies.AllowV2()
c.Policies.SendWhitespaceTag()
c.Policies.WhitespaceStartAKE()
c.SetDebug(true)
// comment out to enable fragmentation
//c.SetFragmentSize(100)
return &peer{
otr: c,
privKey: priv,
rw: rw,
}
}
func (p *peer) fingerprint() string {
return hex.EncodeToString(p.privKey.DefaultFingerprint())
}
func (p *peer) send(in []otr3.ValidMessage) error {
for _, b := range otr3.Bytes(in) {
_, err := p.rw.Write(append(b, EOM))
if err != nil {
return err
}
}
return nil
}
func receive(p *peer, errc chan<- error) {
s := bufio.NewScanner(p.rw)
for s.Scan() {
plain, toSend, err := p.otr.Receive(s.Bytes())
if err != nil {
log.Println("OTR receive error:", err)
}
if len(plain) > 0 {
fmt.Println("->", string(plain))
}
if err := p.send(toSend); err != nil {
errc <- err
}
}
if err := s.Err(); err != nil {
errc <- err
}
}
func send(p *peer, errc chan<- error) {
s := bufio.NewScanner(os.Stdin)
for s.Scan() {
toSend, err := p.otr.Send(s.Bytes())
if err != nil {
log.Println("OTR send error:", err)
}
if err := p.send(toSend); err != nil {
errc <- err
}
}
if err := s.Err(); err != nil {
errc <- err
}
}
// Inspired by Andrew Gerrands TCP chat roulette example (http://talks.golang.org/2012/chat.slide)
package main
import (
"bufio"
"fmt"
"io"
"log"
"net"
"os"
"os/exec"
"regexp"
"sync"
"github.com/juniorz/otrio"
)
const listenAddr = "localhost:4000"
func main() {
l, err := net.Listen("tcp", listenAddr)
if err != nil {
log.Fatal(err)
}
log.Println("listening at", listenAddr)
for {
c, err := l.Accept()
if err != nil {
log.Fatal(err)
}
go match(c)
}
}
var partner = make(chan io.ReadWriteCloser)
func match(c io.ReadWriteCloser) {
fmt.Fprint(c, "Waiting for a partner...")
select {
case partner <- c:
// now handled by the other goroutine
case p := <-partner:
chat(p, c)
}
}
func chat(a, b io.ReadWriteCloser) {
fmt.Fprint(a, "Found one! Say hi.")
fmt.Fprint(b, "Found one! Say hi.")
errc := make(chan error, 1)
go cp(io.MultiWriter(a, lg(), forgeRecipient(a)), b, errc)
go cp(io.MultiWriter(b, lg(), forgeRecipient(b)), a, errc)
if err := <-errc; err != nil {
log.Println("err:", err)
}
a.Close()
b.Close()
}
func cp(w io.Writer, r io.Reader, errc chan<- error) {
_, err := io.Copy(w, r)
errc <- err
}
func lg() io.Writer {
lR, lW := io.Pipe()
go func() {
s := otrio.NewScanner(lR)
for s.Scan() {
fmt.Printf("<-> %q\n", s.Text())
}
}()
return lW
}
func forgeRecipient(rec io.Writer) io.Writer {
recMsg := make(chan []byte)
capKey := make(chan string)
forgedMsg := make(chan forgery)
hR, hW := io.Pipe()
out := hck.Parse(hR, recMsg)
go hck.CaptureRevealedKeys(out, capKey)
go func(h *hacker) {
for {
select {
case m := <-recMsg:
h.saveMessage(m, rec)
case k := <-capKey:
go h.findMesageModifiableWith(k, forgedMsg)
case f := <-forgedMsg:
h.forgetMessagesUntil(f.original)
fmt.Println("<!>", string(f.forged))
f.original.recipient.Write(f.forged)
}
}
}(hck)
return hW
}
var (
revealKeyPattern = regexp.MustCompile("Key [0-9]+: ([a-z0-9]+)")
hck = &hacker{}
dict = map[string]string{
"hi": "TW",
"ok": "TW",
}
)
type hacker struct {
otrForger
}
func (h *hacker) Parse(p io.Reader, rec chan<- []byte) io.Reader {
parser := newOtrParser()
//Send received OTR messages to parser cmd
go func(w io.Writer, r io.Reader) {
s := otrio.NewScanner(r)
for s.Scan() {
_, err := w.Write(s.Bytes())
if err != nil {
continue
}
rec <- s.Bytes()
}
}(parser.in, p)
return parser.out
}
func (h *hacker) CaptureRevealedKeys(parserOut io.Reader, ret chan<- string) {
s := bufio.NewScanner(parserOut)
for s.Scan() {
matches := revealKeyPattern.FindStringSubmatch(s.Text())
if len(matches) > 1 {
ret <- matches[1]
}
}
}
func newOtrCommand(c string, args ...string) (*exec.Cmd, io.WriteCloser, io.ReadCloser) {
cmd := exec.Command(c, args...)
in, err := cmd.StdinPipe()
if err != nil {
panic(err)
}
out, err := cmd.StdoutPipe()
if err != nil {
panic(err)
}
//defer h.out.Close()
return cmd, in, out
}
type otrParser struct {
cmd *exec.Cmd
in io.WriteCloser
out io.ReadCloser
}
func newOtrParser() *otrParser {
cmd, in, out := newOtrCommand("otr_parse")
cmd.Stderr = os.Stderr
if err := cmd.Start(); err != nil {
panic(err)
}
return &otrParser{
cmd: cmd,
in: in,
out: out,
}
}
type otrModify struct {
cmd *exec.Cmd
in io.WriteCloser
out io.ReadCloser
}
func newOtrModify(macKey, oldText, newText, pos string) *otrModify {
cmd, in, out := newOtrCommand("otr_modify", macKey, oldText, newText, pos)
return &otrModify{
cmd: cmd,
in: in,
out: out,
}
}
//TODO: remac any message
//otr_remac mackey sender_instance receiver_instance flags snd_keyid rcp_keyid pubkey counter encdata revealed_mackeys
type pastMessage struct {
message []byte
recipient io.Writer
}
type otrForger struct {
pastMessages []*pastMessage
pastMessagesMutex sync.RWMutex
}
func (h *otrForger) saveMessage(msg []byte, rec io.Writer) {
h.pastMessagesMutex.Lock()
defer h.pastMessagesMutex.Unlock()
h.pastMessages = append(h.pastMessages, &pastMessage{
message: msg,
recipient: rec,
})
}
func (h *otrForger) forgetMessagesUntil(msg *pastMessage) {
h.pastMessagesMutex.Lock()
defer h.pastMessagesMutex.Unlock()
for i, m := range h.pastMessages {
if m == msg {
for j := 0; j < i; j++ {
h.pastMessages[j] = nil
}
h.pastMessages = h.pastMessages[i+1:]
return
}
}
}
func (h *otrForger) findMesageModifiableWith(key string, ret chan<- forgery) {
h.pastMessagesMutex.RLock()
defer h.pastMessagesMutex.RUnlock()
for _, m := range h.pastMessages {
go h.tryModifyMessage(m, key, ret)
}
}
type forgery struct {
original *pastMessage
forged []byte
}
func (h *otrForger) tryModifyMessage(p *pastMessage, macKey string, ret chan<- forgery) {
m := newOtrModify(macKey, "", "", "0")
defer m.out.Close()
if err := m.cmd.Start(); err != nil {
return
}
m.in.Write(p.message)
m.in.Close()
outR := bufio.NewReader(m.out)
line, _, err := outR.ReadLine()
if err != nil && err != io.EOF {
return
}
if err := m.cmd.Wait(); err != nil {
//TODO: remove invalid messages
//see the error message to decide
return
}
ret <- forgery{
original: p,
forged: line,
}
}
// Inspired by Andrew Gerrands TCP chat roulette example (http://talks.golang.org/2012/chat.slide)
package main
import (
"bufio"
"fmt"
"io"
"log"
"net"
)
const listenAddr = "localhost:4000"
func main() {
l, err := net.Listen("tcp", listenAddr)
if err != nil {
log.Fatal(err)
}
log.Println("listening at", listenAddr)
for {
c, err := l.Accept()
if err != nil {
log.Fatal(err)
}
go match(c)
}
}
var partner = make(chan io.ReadWriteCloser)
func match(c io.ReadWriteCloser) {
fmt.Fprintln(c, "Waiting for a partner...")
select {
case partner <- c:
// now handled by the other goroutine
case p := <-partner:
chat(p, c)
}
}
func chat(a, b io.ReadWriteCloser) {
fmt.Fprintln(a, "Found one! Say hi.")
fmt.Fprintln(b, "Found one! Say hi.")
errc := make(chan error, 1)
go cp(a, b, errc)
go cp(b, a, errc)
err := <-errc
log.Println(err)
a.Close()
b.Close()
log.Println("Chat terminated")
}
func cp(w io.Writer, r io.Reader, errc chan<- error) {
pR, pW := io.Pipe()
go logMessages(pR)
_, err := io.Copy(io.MultiWriter(w, pW), r)
errc <- err
}
func logMessages(r io.Reader) {
s := bufio.NewScanner(r)
for s.Scan() {
fmt.Printf("<-> %q\n", s.Text())
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment