sendafriend/main.go

542 lines
16 KiB
Go

package main
import (
"bufio"
"cwtch.im/cwtch/model"
libpeer "cwtch.im/cwtch/peer"
"fmt"
"github.com/sethvargo/go-diceware/diceware"
"math/big"
"os"
"time"
"path"
"os/user"
"strings"
"log"
"io/ioutil"
"sync"
"cwtch.im/cwtch/peer/connections"
"math/rand"
"encoding/base32"
)
func driftoff() {
time.Sleep(time.Second * 7)
os.Exit(1)
}
func addContactEntry(peer libpeer.CwtchPeer, name string, onion string) {
decodedPub, err := base32.StdEncoding.DecodeString(strings.ToUpper(onion[:56]))
if err != nil {
fmt.Printf("couldn't extract public key from %v.onion\n", onion)
os.Exit(1)
}
pp := model.PublicProfile{
name,
decodedPub[:32],
false,
false,
onion}
if peer == nil {
fmt.Printf("peer is nil?!?!?\n")
}
peer.GetProfile().Contacts[onion] = &pp
peer.GetProfile().SetCustomAttribute(onion+"_name", name)
peer.GetProfile().SetCustomAttribute(name+"_onion", onion)
peer.GetProfile().TrustPeer(onion)
}
func exitOnEnter() {
fmt.Println("okay, now have your friend type \"sendafriend seek\" and enter the secret")
fmt.Println("or press enter to cancel the game and exit")
reader := bufio.NewReader(os.Stdin)
reader.ReadString('\n')
os.Exit(0)
}
func printHelp() {
// "group" is a secret feature so please don't add it here yet
fmt.Println("usage: sendafriend [command] [options]\nCommands: help, setname, info, list, add, send, receive, hide, seek")
}
func printGroupHelp() {
fmt.Println("usage: sendafriend group {list, create, import, export, send, listen}")
}
func main() {
if len(os.Args) < 2 {
printHelp()
os.Exit(1)
}
if os.Getenv("SENDAFRIEND_DEBUG") != "YES" {
log.SetFlags(0)
log.SetOutput(ioutil.Discard)
}
// this is a cwtch server used only to make the "pair" command work
// it is completely untrusted and only blinded information is passed through it
// it can point to any ol cwtch group, as long as both people running "pair" are using the same one
hidenseekInviteStr := "torv30I0zJgNiUz57gJlxFsp4PUq47WDoKNCL95GEadaLlEE=EsABCiA1MjQzYjNkNzczMjAwNzc1MDExMDI0MjJkM2NkOGNmZBIgsXZKp350ShZ2l3iqOh4ZhGJmj0Etb5QmkL5iLZno9AQaOGZxcWJkNWxrNzRnbG5zZ292aXVuNm92Z3V0MmNqY252Mm12aWdvamNuc21wd3NucTJ5ZGpmM3FkIkD5x/JNhpLVe982dE9+3mp1IEog6O8yGdrIuUs0Tb6F1Fh1J1zuv1nJBFECuzYyF1yhqNvOn9Z6MatXVi+PKQIO"
// the set of servers to randomly pick from when using "sendafriend group create"
serverList := []string{"fqqbd5lk74glnsgoviun6ovgut2cjcnv2mvigojcnsmpwsnq2ydjf3qd"}
var dirname, filename string
if os.Getenv("SENDAFRIEND_FOLDER") != "" {
dirname = os.Getenv("SENDAFRIEND_FOLDER")
filename = path.Join(dirname, "identity.private")
} else {
usr, err := user.Current()
if err != nil {
fmt.Printf("\nerror: could not load current user: %v\n", err)
os.Exit(1)
}
dirname = path.Join(usr.HomeDir, ".sendafriend")
filename = path.Join(dirname, "identity.private")
}
os.MkdirAll(dirname, 0700)
peer, err := libpeer.LoadCwtchPeer(filename, "be gay do crime")
if err != nil {
fmt.Println("couldn't load your config file, attempting to create a new one now")
names, err := diceware.Generate(1)
peer, err = libpeer.NewCwtchPeer(names[0], "be gay do crime", filename)
if err != nil {
fmt.Println("couldn't create one either :( exiting")
os.Exit(1)
}
}
switch os.Args[1] {
default:
printHelp()
case "help":
printHelp()
case "iam":
if len(os.Args) != 3 {
fmt.Println("example: sendafriend iam alice")
os.Exit(1)
}
peer.GetProfile().Name = os.Args[2]
fmt.Printf("hi, %v!\n", os.Args[2])
case "setname":
if len(os.Args) == 3 {
fmt.Println("example: sendafriend setname alice xyz")
os.Exit(1)
}
_, exists := peer.GetProfile().GetCustomAttribute(os.Args[3] + "_name")
if !exists {
fmt.Printf("um you don't have anyone with that onion address in your list")
os.Exit(1)
}
peer.GetProfile().SetCustomAttribute(os.Args[3]+"_name", os.Args[2])
peer.GetProfile().SetCustomAttribute(os.Args[2]+"_onion", os.Args[3])
//todo: unset old one
fmt.Printf("okay, i'll remember that %v is named %v\n", os.Args[3], os.Args[2])
case "info":
fmt.Printf("your name is currently %v and your address is currently %v\n", peer.GetProfile().Name, peer.GetProfile().Onion)
case "list":
contacts := peer.GetContacts()
for i := range contacts {
attr, _ := peer.GetProfile().GetCustomAttribute(contacts[i] + "_name")
fmt.Printf("%v <%v> %v\n", attr, contacts[i], peer.GetContact(contacts[i]).Trusted)
}
case "add":
if len(os.Args) != 4 {
fmt.Println("example: sendafriend add bob xyz")
os.Exit(1)
}
_, exists := peer.GetProfile().GetCustomAttribute(os.Args[3] + "_name")
if exists {
fmt.Printf("woah you already have someone named %v\n", os.Args[3])
os.Exit(1)
}
addContactEntry(peer, os.Args[2], os.Args[3])
fmt.Printf("okay, added %v <%v>\n", os.Args[2], os.Args[3])
case "send":
if len(os.Args) != 3 {
fmt.Println("example: sendafriend send bob < file.txt")
os.Exit(1)
}
onion, exists := peer.GetProfile().GetCustomAttribute(os.Args[2] + "_onion")
if !exists {
fmt.Printf("you don't seem to have a contact named %v\n", os.Args[2])
}
processData := func(onion string, data []byte) []byte {
return nil
}
peer.SetPeerDataHandler(processData)
fmt.Printf("connecting to %v...\n", os.Args[2])
connection := peer.PeerWithOnion(onion)
fmt.Printf("sending data...\n")
reader := bufio.NewReader(os.Stdin)
packet := make([]byte, 65531)
br, _ := reader.Read(packet)
if br > 65530 {
fmt.Printf("sorry but i can't send more than 65530 bytes at once right now :( errorinn is working on it!\n")
os.Exit(1)
}
connection.SendPacket(packet[:br])
case "receive":
if len(os.Args) != 3 {
fmt.Fprintf(os.Stderr, "example: sendafriend receive bob > file.txt\n")
os.Exit(1)
}
verifyOnion, exists := peer.GetProfile().GetCustomAttribute(os.Args[2] + "_onion")
if !exists {
fmt.Fprintf(os.Stderr, "hmm you don't have a contact named %v\n", os.Args[2])
os.Exit(1)
}
processData := func(onion string, data []byte) []byte {
if onion != verifyOnion {
fmt.Fprintf(os.Stderr, "woah, got some unauthenticated data. discarding it!\n")
os.Exit(1)
}
fmt.Printf("%s", data)
os.Exit(0)
return nil
}
peer.SetPeerDataHandler(processData)
fmt.Fprintf(os.Stderr, "waiting for %v to send...\n", os.Args[2])
err = peer.Listen()
if err != nil {
fmt.Printf("error listening for connections: %v\n", err)
}
case "hide":
if len(os.Args) != 3 {
fmt.Fprintf(os.Stderr, "example: sendafriend hide bob\n")
os.Exit(1)
}
suggestedSecret, _ := diceware.Generate(2)
fmt.Printf("enter a secret that only the two of you would know [%v %v]: ", suggestedSecret[0], suggestedSecret[1])
in := bufio.NewReader(os.Stdin)
secret, _ := in.ReadString('\n')
secret = secret[:len(secret)-1]
if secret == "" {
secret = strings.Join(suggestedSecret, " ")
}
fmt.Println("please wait a moment while some magic happens...")
ephemeralPeer, err := libpeer.NewCwtchPeer("Alice", "alicepass", "")
if err != nil {
fmt.Printf("couldn't create an ephemeral onion address: %v\n", err)
os.Exit(1)
}
groupId, err := ephemeralPeer.ImportGroup(hidenseekInviteStr)
if err != nil {
fmt.Printf("couldn't configure the bbs settings: %v\n", err)
os.Exit(1)
}
group := ephemeralPeer.GetGroup(groupId)
ephemeralPeer.JoinServer(group.GroupServer)
mp := ephemeralPeer.GetServers()
for ; mp[group.GroupServer] != connections.AUTHENTICATED; mp = ephemeralPeer.GetServers() {
time.Sleep(time.Millisecond * 500)
}
err = ephemeralPeer.SendMessageToGroup(groupId, "be gay do crimes")
if err != nil {
fmt.Printf("the hide-n-seek server is down or something? try again i guess; %v\n", err)
os.Exit(1)
}
convos := make(map[string]*Conversation)
processData := func(onion string, data []byte) []byte {
if _, contains := convos[onion]; !contains {
convos[onion] = new(Conversation)
secretInt := new(big.Int)
secretInt.SetBytes([]byte(secret))
convos[onion].smp.secret = secretInt
}
tlvPacket := arr2tlv(data)
if tlvPacket.typ == 42 {
fmt.Printf("you've been found! adding %v <%s> to contact list\n", os.Args[2], tlvPacket.data)
addContactEntry(peer, os.Args[2], string(tlvPacket.data))
peer.Save()
finalPacket := tlv{42, uint16(len(peer.GetProfile().Onion)), []byte(peer.GetProfile().Onion)}
go driftoff()
return tlv2arr(finalPacket)
}
out, _, _ := convos[onion].processSMP(tlvPacket)
return tlv2arr(out)
}
ephemeralPeer.SetPeerDataHandler(processData)
go ephemeralPeer.Listen()
exitOnEnter()
case "seek":
if len(os.Args) != 3 {
fmt.Fprintf(os.Stderr, "example: sendafriend seek alice\n")
os.Exit(1)
}
fmt.Print("enter a secret that only the two of you would know: ")
in := bufio.NewReader(os.Stdin)
secret, _ := in.ReadString('\n')
secret = secret[:len(secret)-1]
fmt.Println("okay, now please wait a moment while some magic happens...")
ephemeralPeer, err := libpeer.NewCwtchPeer("Alice", "alicepass", "")
if err != nil {
fmt.Printf("couldn't create an ephemeral onion address: %v\n", err)
os.Exit(1)
}
groupId, err := ephemeralPeer.ImportGroup(hidenseekInviteStr)
if err != nil {
fmt.Printf("couldn't configure the bbs settings: %v\n", err)
os.Exit(1)
}
group := ephemeralPeer.GetGroup(groupId)
ephemeralPeer.JoinServer(group.GroupServer)
//sadly we cannot listen for messages normally because we want to go in reverse order
messages := group.GetTimeline()
for ; len(messages) == 0; messages = group.GetTimeline() {
time.Sleep(time.Second * 3)
}
var waitGroup sync.WaitGroup
fmt.Printf("got advertisements. checking them...")
for i := len(messages) - 1; i >= 0; i-- {
if messages[i].Message == "be gay do crimes" {
// this is not as scary as it seems because only one of these goroutines will actually use the peer
waitGroup.Add(1)
go doSMP(peer, messages[i].PeerID, secret, os.Args[2])
}
}
waitGroup.Wait()
case "group":
if len(os.Args) < 3 {
printGroupHelp()
os.Exit(1)
}
switch os.Args[2] {
default:
printGroupHelp()
case "help":
printGroupHelp()
case "list":
groups := peer.GetGroups()
for i := range groups {
g := peer.GetGroup(groups[i])
groupName, _ := peer.GetProfile().GetCustomAttribute(g.GroupID + "_groupname")
fmt.Printf("%v <%v@%v> %v\n", groupName, g.GroupID, g.GroupServer, g.Accepted)
}
case "create":
if len(os.Args) != 4 {
fmt.Println("example: sendafriend group create [groupname]")
os.Exit(1)
}
groupId, _, err := peer.StartGroup(serverList[rand.Intn(len(serverList))])
if err != nil {
fmt.Printf("couldn't create a group: %v\n", err)
os.Exit(1)
}
peer.GetProfile().SetCustomAttribute(os.Args[3] + "_groupid", groupId)
peer.GetProfile().SetCustomAttribute(groupId + "_groupname", os.Args[3])
fmt.Printf("created group %v\n", os.Args[3])
case "import":
if len(os.Args) != 5 {
fmt.Println("example: sendafriend group import [groupname] [groupstr]")
os.Exit(1)
}
groupId, err := peer.ImportGroup(os.Args[4])
if err != nil {
fmt.Printf("couldn't import the group: %v\n", err)
os.Exit(1)
}
peer.GetProfile().SetCustomAttribute(os.Args[3] + "_groupid", groupId)
peer.GetProfile().SetCustomAttribute(groupId + "_groupname", os.Args[3])
case "export":
if len(os.Args) != 4 {
fmt.Println("example: sendafriend group export [groupname")
os.Exit(1)
}
groupId, exists := peer.GetProfile().GetCustomAttribute(os.Args[3] + "_groupid")
if !exists {
fmt.Printf("you don't seem to have a group called %v\n", os.Args[3])
os.Exit(1)
}
exportStr, err := peer.ExportGroup(groupId)
if err != nil {
fmt.Printf("couldn't export group: %v\n", err)
os.Exit(1)
}
fmt.Println(exportStr)
case "send":
if len(os.Args) != 4 {
fmt.Println("example: echo \"go!\" | sendafriend group send [groupname]")
os.Exit(1)
}
groupId, exists := peer.GetProfile().GetCustomAttribute(os.Args[3] + "_groupid")
if !exists {
fmt.Printf("you don't seem to have a group called %v\n", os.Args[3])
os.Exit(1)
}
g := peer.GetGroup(groupId)
reader := bufio.NewReader(os.Stdin)
message := make([]byte, 1025)
br, err := reader.Read(message)
if err != nil {
fmt.Println("error reading input")
os.Exit(1)
}
message = message[:br]
if br > 1024 {
fmt.Println("sorry, this feature uses cwtch groups which currently only support up to 1024 byte messages")
os.Exit(1)
}
fmt.Println("sending...")
peer.JoinServer(g.GroupServer)
mp := peer.GetServers()
for ; mp[g.GroupServer] != connections.AUTHENTICATED; mp = peer.GetServers() {
time.Sleep(time.Millisecond * 500)
}
err = peer.SendMessageToGroup(groupId, string(message))
if err != nil {
fmt.Printf("error sending message: %v\n", err)
os.Exit(1)
}
case "listen":
if len(os.Args) != 4 {
fmt.Println("example: sendafriend group listen [groupname]")
os.Exit(1)
}
groupId, exists := peer.GetProfile().GetCustomAttribute(os.Args[3] + "_groupid")
if !exists {
fmt.Printf("you don't seem to have a group called %v\n", os.Args[3])
os.Exit(1)
}
group := peer.GetGroup(groupId)
if group == nil {
fmt.Printf("storage out of sync. please report this bug as it shouldn't have happened\n")
os.Exit(1)
}
group.NewMessage = make(chan model.Message)
peer.JoinServer(group.GroupServer)
for {
m := <-group.NewMessage
fmt.Printf("%v\n", m.Message)
}
case "invite":
if len(os.Args) != 5 {
fmt.Println("example: sendafriend group invite [friendname] [groupname]")
os.Exit(1)
}
onion, exists := peer.GetProfile().GetCustomAttribute(os.Args[3] + "_onion")
if !exists {
fmt.Fprintf(os.Stderr, "hmm you don't have a contact named %v\n", os.Args[3])
os.Exit(1)
}
groupId, exists := peer.GetProfile().GetCustomAttribute(os.Args[4] + "_groupid")
if !exists {
fmt.Printf("you don't seem to have a group called %v\n", os.Args[4])
os.Exit(1)
}
fmt.Printf("attempting to connect to %v...\n", os.Args[3])
peer.PeerWithOnion(onion)
mp := peer.GetPeers()
for ; mp[onion] != connections.AUTHENTICATED; mp = peer.GetPeers() {
time.Sleep(time.Millisecond * 500)
}
err = peer.InviteOnionToGroup(onion, groupId)
if err != nil {
fmt.Printf("Trusted: %v\n", peer.GetContact(onion).Trusted)
fmt.Printf("failed to invite %v to %v: %v\n", os.Args[3], os.Args[4], err)
os.Exit(1)
}
case "accept":
if len(os.Args) != 5 {
fmt.Println("example: sendafriend group accept [friendname] [groupname]")
os.Exit(1)
}
onion, exists := peer.GetProfile().GetCustomAttribute(os.Args[3] + "_onion")
if !exists {
fmt.Fprintf(os.Stderr, "hmm you don't have a contact named %v\n", os.Args[3])
os.Exit(1)
}
_, exists = peer.GetProfile().GetCustomAttribute(os.Args[4] + "_groupid")
if exists {
fmt.Printf("oops, you already have a group called %v\n", os.Args[4])
os.Exit(1)
}
s := len(peer.GetGroups())
go peer.Listen()
fmt.Println("okay, have a friend run \"sendafriend group invite [yourname] [groupname]\"")
for {
n := len(peer.GetGroups())
if n == s {
time.Sleep(time.Millisecond * 500)
continue
}
groups := peer.GetGroups()
for i := range groups {
if peer.GetGroup(groups[i]).Owner == onion && !peer.GetGroup(groups[i]).Accepted {
fmt.Printf("adding group %v!\n", os.Args[4])
peer.AcceptInvite(groups[i])
peer.GetProfile().SetCustomAttribute(os.Args[4] + "_groupid", groups[i])
peer.GetProfile().SetCustomAttribute(groups[i] + "_groupname", os.Args[4])
peer.Save()
os.Exit(0)
}
}
}
}
}
peer.Save()
}