Skip to content

Commit

Permalink
Completely overhaul the internals
Browse files Browse the repository at this point in the history
This is much more structured, easier to follow, and the individual parts
are more composable.
  • Loading branch information
skeeto committed Jul 13, 2019
1 parent 3ac66b7 commit 9419bff
Show file tree
Hide file tree
Showing 7 changed files with 795 additions and 576 deletions.
72 changes: 40 additions & 32 deletions README.md
Expand Up @@ -26,37 +26,37 @@ Requires Go 1.9 or later.

Just pipe the output straight into GnuPG:

$ passphrase2pgp -uid "Real Name <name@example.com>" | gpg --import
$ passphrase2pgp -u "Real Name <name@example.com>" | gpg --import

**The `-uid` argument is required.** It's also used as an input during
key generation, so to reproduce the same key, you will need to use
**Either `-u` or `-l` is required.** The `-u` argument is used during
key generation, so to reproduce the same key you will need to use
exactly the same passphrase *and* User ID.

Use `-help` for an option listing:
There are two modes of operation: key generation (`-K`, default) and
detached signatures (`-S`).

Usage of passphrase2pgp:
-date int
creation date (unix epoch seconds)
-fingerprint
also show fingerprint
-load string
load key from file instead of generating
-now
use current time as creation date
-paranoid
paranoid mode
-passphrase-file string
Use `-h` for an option listing:

Usage of ./passphrase2pgp:
-K output a new key (default true)
-S output detached signature for input
-a use ASCII armor
-f also show fingerprint
-h print this help message
-i string
read passphrase from file
-public
only output public key
-repeat uint
-l string
load key from file instead
-n use current time as creation date
-p only output public key
-r int
number of repeated passphrase prompts (default 1)
-sign
output detached signature for input
-subkey
also output an encryption subkey
-uid string
key user ID (required)
-s also output encryption subkey
-t int
creation date (unix epoch seconds)
-u string
user ID for the key
-x paranoid mode

Per the OpenPGP specification, **the Key ID is a hash over both the key
and its creation date.** Therefore using a different date with the same
Expand All @@ -67,8 +67,8 @@ with `-date` or `-now`, but, to regenerate the same key in the future,
you will need to use `-date` to reenter the exact time. If 1970 is a
problem, then choose another memorable date.

The `-paranoid` setting quadruples the KDF difficulty. This will result
in a different key for the same passphrase.
The `-x` (paranoid) setting quadruples the KDF difficulty. This will
result in a different key for the same passphrase.

Once your key is generated, you may want to secure it with a protection
passphrase on your GnuPG keychain in order to protect it at rest:
Expand All @@ -83,15 +83,23 @@ primary key as trusted.
$ gpg --edit-key "Real Name"
gpg> trust

It's also possible create detached signatures with passphrase2pgp:
The "sign" mode (`-S`) creates detached signatures:

$ passphrase2pgp -S -u "Real Name <name@example.com>" <data >data.sig
passphrase:
passphrase (repeat):

To perform multiple operations at once without regenerating the key for
each operation, the load (`-l`) option exists to load a previously
generated key:

$ passphrase2pgp -uid "Real Name" >secret.pgp
$ passphrase2pgp -u "Real Name <name@example.com>" >secret.pgp
passphrase:
passphrase (repeat):
$ passphrase2pgp -load secret.pgp -public >Real-Name.asc
$ passphrase2pgp -load secret.pgp -sign <data >data.sig
$ passphrase2pgp -l secret.pgp -a -p >Real-Name.asc
$ passphrase2pgp -S -l secret.pgp <data >data.sig

Where `Real-Name.pgp`, `data`, and `data.sig` are distributed to others.
Where `Real-Name.asc`, `data`, and `data.sig` are distributed to others.
Consuming these in GnuPG:

$ gpg --import Real-Name.asc
Expand Down
99 changes: 99 additions & 0 deletions armor.go
@@ -0,0 +1,99 @@
package main

import (
"bytes"
"encoding/base64"
"io"
)

// Armor returns the ASCII armored version of its input packet. It
// autodetects what kind of armor should be used based on the packet
// header.
func Armor(buf []byte) []byte {
const (
pubBeg = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n\n"
pubEnd = "\n-----END PGP PUBLIC KEY BLOCK-----\n"
secBeg = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n\n"
secEnd = "\n-----END PGP PRIVATE KEY BLOCK-----\n"
sigBeg = "-----BEGIN PGP SIGNATURE-----\n\n"
sigEnd = "\n-----END PGP SIGNATURE-----\n"
)

var beg, end string
switch buf[0] {
case 0xc0 | 2:
beg = sigBeg
end = sigEnd
case 0xc0 | 5:
beg = secBeg
end = secEnd
case 0xc0 | 6:
beg = pubBeg
end = pubEnd
}

var asc bytes.Buffer
asc.WriteString(beg)
wrap := &wrapper{&asc, 64, 0}
b64 := base64.NewEncoder(base64.RawStdEncoding, wrap)
b64.Write(buf)
b64.Close()
asc.WriteString("\n=")
b64 = base64.NewEncoder(base64.RawStdEncoding, &asc)
crc := crc24(buf)
b64.Write([]byte{byte(crc >> 16), byte(crc >> 8), byte(crc)})
b64.Close()
asc.WriteString(end)
return asc.Bytes()
}

// Return CRC-24 checksum for a buffer.
func crc24(buf []byte) int32 {
const (
crc24Init = 0x0b704ce
crc24Poly = 0x1864cfb
)
var crc int32 = crc24Init
for _, b := range buf {
crc ^= int32(b) << 16
for i := 0; i < 8; i++ {
crc <<= 1
if crc&0x1000000 != 0 {
crc ^= crc24Poly
}
}
}
return crc & 0xFFFFFF
}

// wrapper is an io.Writer filter that inserts regular hard line breaks.
type wrapper struct {
w io.Writer
max int
count int
}

func (w *wrapper) Write(p []byte) (int, error) {
for len(p) > 0 {
if w.count == w.max {
if _, err := w.w.Write([]byte{10}); err != nil {
return 0, err
}
w.count = 0
}
left := w.max - w.count
var line []byte
if len(p) > left {
line = p[:left]
} else {
line = p
}
p = p[len(line):]
w.count += len(line)
_, err := w.w.Write(line)
if err != nil {
return 0, err
}
}
return len(p), nil
}
130 changes: 130 additions & 0 deletions encryptkey.go
@@ -0,0 +1,130 @@
package main

import (
"encoding/binary"

"golang.org/x/crypto/curve25519"
)

const (
// EncryptKeyPubLen is the size of the public part of an OpenPGP packet.
EncryptKeyPubLen = 58
)

// EncryptKey represents an X25519 Diffie-Hellman key (ECDH). Implements
// Bindable.
type EncryptKey struct {
Key []byte
created int64
packet []byte
}

// Seed sets the 32-byte seed for a sign key.
func (k *EncryptKey) Seed(seed []byte) {
var pubkey [32]byte
var seckey [32]byte
copy(seckey[:], seed)
seckey[0] &= 248
seckey[31] &= 127
seckey[31] |= 64
curve25519.ScalarBaseMult(&pubkey, &seckey)
k.Key = append(seckey[:], pubkey[:]...)
k.packet = nil
}

// Created returns the key's creation date in unix epoch seconds.
func (k *EncryptKey) Created() int64 {
return k.created
}

// SetCreated sets the creation date in unix epoch seconds.
func (k *EncryptKey) SetCreated(time int64) {
k.created = time
k.packet = nil
}

// Seckey returns the secret key portion of this key.
func (k *EncryptKey) Seckey() []byte {
return k.Key[:32]
}

// Pubkey returns the public key portion of this key.
func (k *EncryptKey) Pubkey() []byte {
return k.Key[32:]
}

// Packet returns the OpenPGP packet encoding this key.
func (k *EncryptKey) Packet() []byte {
const encryptKeySecLen = 3 + 32 + 2
total := EncryptKeyPubLen + encryptKeySecLen
be := binary.BigEndian

if k.packet != nil {
return k.packet
}

packet := make([]byte, EncryptKeyPubLen+1, total)
packet[0] = 0xc0 | 7 // packet header, Secret-Subkey Packet (7)
packet[2] = 0x04 // packet version, new (4)

// Public Key
be.PutUint32(packet[3:7], uint32(k.created)) // creation date
packet[7] = 18 // algorithm, Elliptic Curve
packet[8] = 10 // OID length
// OID (1.3.6.1.4.1.3029.1.5.1)
oid := []byte{0x2b, 0x06, 0x01, 0x04, 0x01, 0x97, 0x55, 0x01, 0x05, 0x01}
copy(packet[9:19], oid)
be.PutUint16(packet[19:21], 263) // public key length (always 263 bits)
packet[21] = 0x40 // MPI prefix
copy(packet[22:54], k.Pubkey())
// KDF parameters
packet[54] = 3 // length
packet[55] = 0x01 // reserved (1)
packet[56] = 0x08 // SHA-256
packet[57] = 0x07 // AES-128? (spec is incorrect)

// Secret Key
packet[58] = 0 // string-to-key, unencrypted
// append MPI-encoded key
mpikey := mpi(reverse(k.Seckey()))
packet = append(packet, mpikey...)
// compute and append checksum
var checksum uint16
for _, b := range mpikey {
checksum += uint16(b)
}
packet = packet[:len(packet)+2]
be.PutUint16(packet[len(packet)-2:], checksum)

packet[1] = byte(len(packet) - 2) // packet length
k.packet = packet
return packet
}

// PubPacket returns an OpenPGP public key packet for this key.
func (k *EncryptKey) PubPacket() []byte {
packet := make([]byte, EncryptKeyPubLen)
packet[0] = 0xc0 | 14 // packet header, Public-Subkey packet (14)
packet[1] = EncryptKeyPubLen - 2
copy(packet[2:], k.Packet()[2:])
return packet
}

func (k *EncryptKey) SignType() byte {
return 0x18
}

func (k *EncryptKey) SignData() []byte {
prefix := []byte{0x99, 0, 56}
packet := k.PubPacket()[2:]
return append(prefix, packet...)
}

// Returns a reversed copy of its input.
func reverse(b []byte) []byte {
c := make([]byte, len(b))
for i, v := range b {
c[len(c)-i-1] = v
}
return c
}

0 comments on commit 9419bff

Please sign in to comment.