Skip to content

Commit

Permalink
simplify the codebase
Browse files Browse the repository at this point in the history
  • Loading branch information
ysmood committed Jan 1, 2024
1 parent 8800624 commit 3b2f8ee
Show file tree
Hide file tree
Showing 55 changed files with 1,861 additions and 1,056 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
*.out
*.txt
*.wsp
tmp/
84 changes: 26 additions & 58 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,18 @@
<!-- markdownlint-disable MD010 -->

# Overview

A simple lib to encrypt, decrypt data with [Public-key cryptography](https://en.wikipedia.org/wiki/Public-key_cryptography).
Now only [ED25519](https://en.wikipedia.org/wiki/EdDSA#Ed25519), [ECDSA](https://en.wikipedia.org/wiki/Elliptic_Curve_Digital_Signature_Algorithm),
Now [ED25519](https://en.wikipedia.org/wiki/EdDSA#Ed25519), [ECDSA](https://en.wikipedia.org/wiki/Elliptic_Curve_Digital_Signature_Algorithm),
and [RSA](<https://en.wikipedia.org/wiki/RSA_(cryptosystem)>) are supported.

Features:

- Small wire format.
- Reuse the existing ssh key pairs and github public key url.
- Auto find the right key to decrypt.
- Encrypt data for multiple recipients.
- ssh-agent like server to cache the private key passphrase.
- Full stream design for large data.

## CLI tool

### Installation
Expand All @@ -26,80 +33,41 @@ Here is a simple example to encrypt and decrypt for yourself, the encrypted data

```bash
# generate a key pair
ssh-keygen -t ed25519
ssh-keygen -t ed25519 -N "" -f id_ed25519

echo 'hello world!' > plain
echo 'hello world!' > hello.txt

# Encrypt file plain to file encrypted
# Encrypt file hello.txt to a whisper file hello.wsp .
# It will auto start a agent server to cache the passphrase so you don't have to retype it.
whisper plain > encrypted
whisper -e='id_ed25519.pub' hello.txt > hello.wsp

# Decrypt file encrypted to stdout
whisper -d encrypted
whisper -p='id_ed25519' hello.wsp
# hello world!

# You can also use it as a pipe
cat plain | whisper > encrypted
cat encrypted | whisper -d
```

Here is an example to encrypt and decrypt for others, the encrypted data can only be decrypted by their public key.
Suppose we have key pair for Jack `jack.pub` and `jack`, and key pair for Tim `tim.pub` and `tim`.

```bash
# Encrypt file that can only be decrypted by Tim
whisper -k 'jack' -p='tim.pub' plain > encrypted

# Decrypt file encrypted to stdout
whisper -d -k='tim' -p 'jack' encrypted
cat hello.txt | whisper -e='id_ed25519.pub' > hello.wsp
cat hello.wsp | whisper -p='id_ed25519'
```

You can also use a url for a remote public key file.
Here we use my public key on github to encrypt the data.
Github generally exposes your public key file at `@https://github.com/{YOUR_ID}.keys`.

```bash
whisper -p='@https://github.com/ysmood.keys' plain > encrypted
# For github you can use the user id directly.
whisper -e='@ysmood' hello.txt > hello.wsp

# A shortcut the same as above
whisper -p='@ysmood' plain > encrypted
# For other sites you can use the full url.
whisper -e='@https://github.com/ysmood.keys' hello.txt > hello.wsp

# A authorized_keys file may contain several keys, you can add a suffix to select a specific key.
# 'tbml' is the substring of the key content we want to use.
whisper -p='@ysmood:ed25519' plain > encrypted
# A authorized_keys file may contain several keys, you can add a suffix to select a specific key to encrypt.
# 'ed25519' is the substring of the key we want to use.
whisper -e='@ysmood:ed25519' hello.txt > hello.wsp

# Encrypt content for multiple recipients, such as Jack and Tim.
whisper -a='@ysmood' -p='@jack' -p='@tim' plain > encrypted

# Or embed the default public key file to the output.
whisper -a=. -p='@jack' -p='@tim' plain > encrypted
whisper -e='@jack' -e='@tim' hello.txt > hello.wsp

# Decrypt on Jack's machine, the machine has Jack's private key.
whisper -d encrypted
```

The wire format output of the:

```bash
whisper -a='@ysmood' -p='@jack' -p='@tim' plain > encrypted
```

looks like this:

```txt
@ysmood @jack @tim ,AQIivDFghr38p3YaVyGB3M3-vsxraWWL
```

The output has 2 parts: header and body, they are separated by a comma `,`.

In the header, each public key id is separated by space. The first one is the sender's, the rest are the recipients'.
They will be plaintext, so you can quickly know who can decrypt the data and verify if the data is malicious by the sender's public key.

The body is usually a base64 encoded string, it's the encrypted data, even without the header,
as long as you have the public key of the sender, and the private key of the recipient, you can decrypt the data, for example:

```bash
whisper -d -k='~/.ssh/id_ed25519_jack' -p='@ysmood' encrypted
whisper hello.wsp
```

The `id_ed25519_jack` is the private key of Jack, the `@ysmood` is the public key of the sender.
167 changes: 23 additions & 144 deletions agent.go
Original file line number Diff line number Diff line change
@@ -1,26 +1,28 @@
package main

import (
"encoding/base64"
"io"
"log"
"os"
"os/exec"
"strings"
"time"

whisper "github.com/ysmood/whisper/lib"
"github.com/ysmood/whisper/lib/secure"
)

func runAsAgent() {
log.Println("whisper agent started, version:", whisper.Version())
log.Println("whisper agent started, version:", int(whisper.Version))

whisper.NewAgentServer().Serve(WHISPER_AGENT_ADDR)
}

func startAgent() {
if whisper.IsAgentRunning(WHISPER_AGENT_ADDR, whisper.Version()) {
running, err := whisper.IsAgentRunning(WHISPER_AGENT_ADDR, whisper.Version)
if err != nil {
panic(err)
}

if running {
return
}

Expand All @@ -43,161 +45,38 @@ func startAgent() {

log.Println("wait for background whisper agent to start ...")

for !whisper.IsAgentRunning(WHISPER_AGENT_ADDR, whisper.Version()) {
time.Sleep(time.Millisecond * 100)
}

log.Println("background whisper agent started")
}

func agentCheckPassphrase(prv whisper.PrivateKey) bool {
return whisper.IsPassphraseRight(WHISPER_AGENT_ADDR, prv)
}

type PublicKeyMeta struct {
Sender string
Receivers publicKeysFlag
}

func agentWhisper(decrypt bool, pubKeyMeta PublicKeyMeta, conf whisper.Config, inFile, outFile string) {
in := getInput(inFile)
defer func() { _ = in.Close() }()

out := getOutput(outFile)
defer func() { _ = out.Close() }()

req := whisper.AgentReq{Decrypt: decrypt, Config: conf}

if decrypt {
pub := extractSender(in)
if len(req.Config.Public) == 0 {
req.Config.Public = append(req.Config.Public, pub)
}
extractReceivers(in)
} else {
req.PublicKey = prefixSender(pubKeyMeta.Sender, out)
prefixReceivers(pubKeyMeta.Receivers, out)
}

whisper.CallAgent(WHISPER_AGENT_ADDR, req, in, out)
}

// If there's no public key, the output will be prefixed with "_".
// If the public key is remote, the output will be prefixed with "@", the prefix will end with space.
// If the public key is local, the output will be prefixed with ".", the prefix will end with space.
func prefixSender(sender string, out io.Writer) secure.KeyWithFilter {
if sender == "." {
sender = pubKeyName(DEFAULT_KEY_NAME)
}

if sender == "" {
_, err := out.Write([]byte("_ "))
for {
running, err := whisper.IsAgentRunning(WHISPER_AGENT_ADDR, whisper.Version)
if err != nil {
panic(err)
}
return secure.KeyWithFilter{}
}

key := getPublicKey(sender)

_, remote := extractRemotePublicKey(sender)

var err error
if remote {
_, err = out.Write([]byte(sender))
} else {
_, err = out.Write([]byte("." + base64.StdEncoding.EncodeToString(key.Key) + ":" + key.Filter))
}
if err != nil {
panic(err)
}

_, err = out.Write([]byte(" "))
if err != nil {
panic(err)
}

return key
}

func prefixReceivers(receivers publicKeysFlag, out io.Writer) {
for _, receiver := range receivers {
if receiver[0] != '@' {
continue
if running {
break
}

_, err := out.Write([]byte(receiver + " "))
if err != nil {
panic(err)
}
time.Sleep(time.Millisecond * 100)
}

_, err := out.Write([]byte(","))
if err != nil {
panic(err)
}
log.Println("background whisper agent started")
}

func extractSender(in io.Reader) secure.KeyWithFilter {
buf := make([]byte, 1)
_, err := in.Read(buf)
func agentCheckPassphrase(prv whisper.PrivateKey) bool {
r, err := whisper.IsPassphraseRight(WHISPER_AGENT_ADDR, prv)
if err != nil {
panic(err)
}

getRawPrefix := func() string {
raw := []byte{}
for {
_, err := in.Read(buf)
if err != nil {
panic(err)
}

if buf[0] == ' ' {
break
}

raw = append(raw, buf[0])
}

return string(raw)
}

switch buf[0] {
case '@':
raw := getRawPrefix()
return getPublicKey("@" + raw)
case '.':
raw := strings.Split(getRawPrefix(), ":")
rawKey, filter := raw[0], raw[1]

key, err := base64.StdEncoding.DecodeString(rawKey)
if err != nil {
panic(err)
}

return secure.KeyWithFilter{
Key: key,
Filter: filter,
}
default:
return secure.KeyWithFilter{
Key: getKey(pubKeyName(DEFAULT_KEY_NAME)),
}
}
return r
}

func extractReceivers(in io.Reader) {
buf := make([]byte, 1)
func agentWhisper(decrypt bool, conf whisper.Config, in io.ReadCloser, out io.WriteCloser) {
defer func() { _ = in.Close() }()
defer func() { _ = out.Close() }()

for {
_, err := in.Read(buf)
if err != nil {
panic(err)
}
req := whisper.AgentReq{Decrypt: decrypt, Config: conf}

if buf[0] == ',' {
break
}
err := whisper.CallAgent(WHISPER_AGENT_ADDR, req, in, out)
if err != nil {
panic(err)
}
}
16 changes: 14 additions & 2 deletions constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,27 @@ import (
"github.com/ysmood/goe"
)

var DEFAULT_KEY_NAME = func() string {
var SSH_DIR = func() string {
p, err := os.UserHomeDir()
if err != nil {
panic(err)
}

return filepath.Join(p, ".ssh", "id_ed25519")
return filepath.Join(p, ".ssh")
}()

var WHISPER_DEFAULT_KEY = func() string {
key := os.Getenv("WHISPER_DEFAULT_KEY")

if key == "" {
key = filepath.Join(SSH_DIR, "id_ed25519")
}

return key
}()

var WHISPER_PASSPHRASE = os.Getenv("WHISPER_PASSPHRASE")

var WHISPER_AGENT_ADDR = goe.Get("WHISPER_AGENT_ADDR", "127.0.0.1:57217")

var AGENT_FLAG = "run-as-agent"
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,6 @@ require (
github.com/ysmood/goe v0.2.0
github.com/ysmood/gop v0.2.0 // indirect
golang.org/x/crypto v0.16.0
golang.org/x/sync v0.5.0
golang.org/x/sys v0.15.0 // indirect
)
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ github.com/ysmood/got v0.38.3 h1:yGvt4EnVbwK2k1jjz73ztT47f/RrqHWGUMxGx6Vvu9w=
github.com/ysmood/got v0.38.3/go.mod h1:W7DdpuX6skL3NszLmAsC5hT7JAhuLZhByVzHTq874Qg=
golang.org/x/crypto v0.16.0 h1:mMMrFzRSCF0GvB7Ne27XVtVAaXLrPmgPC7/v0tkwHaY=
golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE=
golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4=
Expand Down
14 changes: 14 additions & 0 deletions lib/.got/snapshots/TestMeta/meta.gop
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
&whisper.Meta{
Gzip: true,
Sign: true,
LongPubKeyHash: false,
Sender: &whisper.PublicKey{
Data: []uint8(nil),
ID: "test",
Selector: "abc",
},
PubKeyHashList: map[string]int{
"\x13\xc1\xf8\xa1": 0,
"\x9b!\xf0\x10": 1,
},
}
Loading

0 comments on commit 3b2f8ee

Please sign in to comment.