Refactor api (#6)

* Refactor library, remove duplicates

* Rebuild structure to use Messages and Signature models

* Use PGPSplitMessage

* Remove signature model

* Various fixes

* Add helpers with tests

* Fixes, add some docs, add tests

* Add attachment helpers

* Add helpers Symmetric encryption

* Edit docs + examples

* Rename kr to keyRing

* Various fixes for documentation

* Edit JSON handling functions, add decrypt keyring via token

* Add proposal changes doc

* Fix CI

* Drop *Message functions, join CleartextMessage and BinaryMessage

* Change canonicalization and trimming only to text signatures

* Add cleartextsignature, detach signature from message model, move helpers

* Documentation, remove optional parameters

* Move verification to separate model

* Don't return message in VerifyDetached

* Update table of contents in readme

* Appease golint

* Run go fmt

* Rename Encrypt/DecryptMessageWithPassword to ..WithToken

These functions shouldn't be used with user-provided passwords,
as they don't do any key-stretching.

* Change key generation usernames
This commit is contained in:
wussler 2019-06-03 17:00:01 +02:00 committed by GitHub
parent 82d49bf235
commit e65ed17b41
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
34 changed files with 2573 additions and 1478 deletions

22
helper/base_test.go Normal file
View file

@ -0,0 +1,22 @@
package helper
import (
"io/ioutil"
"strings"
)
var err error
func readTestFile(name string, trimNewlines bool) string {
data, err := ioutil.ReadFile("../crypto/testdata/" + name)
if err != nil {
panic(err)
}
if trimNewlines {
return strings.TrimRight(string(data), "\n")
}
return string(data)
}
// Corresponding key in ../crypto/testdata/keyring_privateKey
const testMailboxPassword = "apple"

82
helper/cleartext.go Normal file
View file

@ -0,0 +1,82 @@
package helper
import (
"errors"
"strings"
"github.com/ProtonMail/gopenpgp/armor"
"github.com/ProtonMail/gopenpgp/crypto"
"github.com/ProtonMail/gopenpgp/internal"
)
// SignCleartextMessageArmored signs text given a private key and its passphrase, canonicalizes and trims the newlines,
// and returns the PGP-compliant special armoring
func SignCleartextMessageArmored(privateKey, passphrase, text string) (string, error) {
signingKeyRing, err := pgp.BuildKeyRingArmored(privateKey)
if err != nil {
return "", err
}
err = signingKeyRing.UnlockWithPassphrase(passphrase)
if err != nil {
return "", err
}
return SignCleartextMessage(signingKeyRing, text)
}
// VerifyCleartextMessageArmored verifies PGP-compliant armored signed plain text given the public key
// and returns the text or err if the verification fails
func VerifyCleartextMessageArmored(publicKey, armored string, verifyTime int64) (string, error) {
verifyKeyRing, err := pgp.BuildKeyRingArmored(publicKey)
if err != nil {
return "", err
}
return VerifyCleartextMessage(verifyKeyRing, armored, verifyTime)
}
// SignCleartextMessage signs text given a private keyring, canonicalizes and trims the newlines,
// and returns the PGP-compliant special armoring
func SignCleartextMessage(keyRing *crypto.KeyRing, text string) (string, error) {
text = canonicalizeAndTrim(text)
message := crypto.NewPlainMessageFromString(text)
signature, err := keyRing.SignDetached(message)
if err != nil {
return "", err
}
return armor.ArmorClearSignedMessage(message.GetBinary(), signature.GetBinary())
}
// VerifyCleartextMessage verifies PGP-compliant armored signed plain text given the public keyring
// and returns the text or err if the verification fails
func VerifyCleartextMessage(keyRing *crypto.KeyRing, armored string, verifyTime int64) (string, error) {
text, signatureData, err := armor.ReadClearSignedMessage(armored)
if err != nil {
return "", err
}
message := crypto.NewPlainMessageFromString(text)
signature := crypto.NewPGPSignature(signatureData)
ver, err := keyRing.VerifyDetached(message, signature, verifyTime)
if err != nil {
return "", err
}
if !ver.IsValid() {
return "", errors.New("gopenpgp: unable to verify attachment")
}
return message.GetString(), nil
}
// ----- INTERNAL FUNCTIONS -----
// canonicalizeAndTrim alters a string canonicalizing and trimming the newlines
func canonicalizeAndTrim(text string) string {
text = internal.TrimNewlines(text)
text = strings.Replace(strings.Replace(text, "\r\n", "\n", -1), "\n", "\r\n", -1)
return text
}

45
helper/cleartext_test.go Normal file
View file

@ -0,0 +1,45 @@
package helper
import (
"regexp"
"testing"
"github.com/stretchr/testify/assert"
)
const signedPlainText = "Signed message\n"
const testTime = 1557754627 // 2019-05-13T13:37:07+00:00
var signedMessageTest = regexp.MustCompile(
"(?s)^-----BEGIN PGP SIGNED MESSAGE-----.*-----BEGIN PGP SIGNATURE-----.*-----END PGP SIGNATURE-----$")
func TestSignClearText(t *testing.T) {
// Password defined in base_test
armored, err := SignCleartextMessageArmored(
readTestFile("keyring_privateKey", false),
testMailboxPassword,
signedPlainText,
)
if err != nil {
t.Fatal("Cannot armor message:", err)
}
assert.Regexp(t, signedMessageTest, armored)
verified, err := VerifyCleartextMessageArmored(
readTestFile("keyring_publicKey", false),
armored,
pgp.GetUnixTime(),
)
if err != nil {
t.Fatal("Cannot verify message:", err)
}
assert.Exactly(t, canonicalizeAndTrim(signedPlainText), verified)
}
func TestMessageCanonicalizeAndTrim(t *testing.T) {
text := "Hi \ntest!\r\n\n"
canon := canonicalizeAndTrim(text)
assert.Exactly(t, "Hi\r\ntest!\r\n\r\n", canon)
}

258
helper/helper.go Normal file
View file

@ -0,0 +1,258 @@
package helper
import (
"errors"
"github.com/ProtonMail/gopenpgp/constants"
"github.com/ProtonMail/gopenpgp/crypto"
)
var pgp = crypto.GetGopenPGP()
// EncryptMessageWithToken encrypts a string with a passphrase using AES256
func EncryptMessageWithToken(
passphrase, plaintext string,
) (ciphertext string, err error) {
return EncryptMessageWithTokenAlgo(passphrase, plaintext, constants.AES256)
}
// EncryptMessageWithTokenAlgo encrypts a string with a random token and an algorithm chosen from constants.*
func EncryptMessageWithTokenAlgo(
token, plaintext, algo string,
) (ciphertext string, err error) {
var pgpMessage *crypto.PGPMessage
var message = crypto.NewPlainMessageFromString(plaintext)
var key = crypto.NewSymmetricKeyFromToken(token, algo)
if pgpMessage, err = key.Encrypt(message); err != nil {
return "", err
}
if ciphertext, err = pgpMessage.GetArmored(); err != nil {
return "", err
}
return ciphertext, nil
}
// DecryptMessageWithToken decrypts an armored message with a random token.
// The algorithm is derived from the armoring.
func DecryptMessageWithToken(token, ciphertext string) (plaintext string, err error) {
var message *crypto.PlainMessage
var pgpMessage *crypto.PGPMessage
var key = crypto.NewSymmetricKeyFromToken(token, "")
if pgpMessage, err = crypto.NewPGPMessageFromArmored(ciphertext); err != nil {
return "", err
}
if message, err = key.Decrypt(pgpMessage); err != nil {
return "", err
}
return message.GetString(), nil
}
// EncryptMessageArmored generates an armored PGP message given a plaintext and an armored public key
func EncryptMessageArmored(publicKey, plaintext string) (ciphertext string, err error) {
var publicKeyRing *crypto.KeyRing
var pgpMessage *crypto.PGPMessage
var message = crypto.NewPlainMessageFromString(plaintext)
if publicKeyRing, err = pgp.BuildKeyRingArmored(publicKey); err != nil {
return "", err
}
if pgpMessage, err = publicKeyRing.Encrypt(message, nil); err != nil {
return "", err
}
if ciphertext, err = pgpMessage.GetArmored(); err != nil {
return "", err
}
return ciphertext, nil
}
// EncryptSignMessageArmored generates an armored signed PGP message given a plaintext and an armored public key
// a private key and its passphrase
func EncryptSignMessageArmored(
publicKey, privateKey, passphrase, plaintext string,
) (ciphertext string, err error) {
var publicKeyRing, privateKeyRing *crypto.KeyRing
var pgpMessage *crypto.PGPMessage
var message = crypto.NewPlainMessageFromString(plaintext)
if publicKeyRing, err = pgp.BuildKeyRingArmored(publicKey); err != nil {
return "", err
}
if privateKeyRing, err = pgp.BuildKeyRingArmored(privateKey); err != nil {
return "", err
}
if err = privateKeyRing.UnlockWithPassphrase(passphrase); err != nil {
return "", err
}
if pgpMessage, err = publicKeyRing.Encrypt(message, privateKeyRing); err != nil {
return "", err
}
if ciphertext, err = pgpMessage.GetArmored(); err != nil {
return "", err
}
return ciphertext, nil
}
// DecryptMessageArmored decrypts an armored PGP message given a private key and its passphrase
func DecryptMessageArmored(
privateKey, passphrase, ciphertext string,
) (plaintext string, err error) {
var privateKeyRing *crypto.KeyRing
var pgpMessage *crypto.PGPMessage
var message *crypto.PlainMessage
if privateKeyRing, err = pgp.BuildKeyRingArmored(privateKey); err != nil {
return "", err
}
if err = privateKeyRing.UnlockWithPassphrase(passphrase); err != nil {
return "", err
}
if pgpMessage, err = crypto.NewPGPMessageFromArmored(ciphertext); err != nil {
return "", err
}
if message, _, err = privateKeyRing.Decrypt(pgpMessage, nil, 0); err != nil {
return "", err
}
return message.GetString(), nil
}
// DecryptVerifyMessageArmored decrypts an armored PGP message given a private key and its passphrase
// and verifies the embedded signature.
// Returns the plain data or an error on signature verification failure.
func DecryptVerifyMessageArmored(
publicKey, privateKey, passphrase, ciphertext string,
) (plaintext string, err error) {
var publicKeyRing, privateKeyRing *crypto.KeyRing
var pgpMessage *crypto.PGPMessage
var message *crypto.PlainMessage
var verification *crypto.Verification
if publicKeyRing, err = pgp.BuildKeyRingArmored(publicKey); err != nil {
return "", err
}
if privateKeyRing, err = pgp.BuildKeyRingArmored(privateKey); err != nil {
return "", err
}
if err = privateKeyRing.UnlockWithPassphrase(passphrase); err != nil {
return "", err
}
if pgpMessage, err = crypto.NewPGPMessageFromArmored(ciphertext); err != nil {
return "", err
}
if message, verification, err = privateKeyRing.Decrypt(pgpMessage, publicKeyRing, pgp.GetUnixTime()); err != nil {
return "", err
}
if !verification.IsValid() {
return "", errors.New("gopenpgp: unable to verify message")
}
return message.GetString(), nil
}
// EncryptSignAttachment encrypts an attachment using a detached signature, given a publicKey, a privateKey
// and its passphrase, the filename, and the unencrypted file data.
// Returns keypacket, dataPacket and unarmored (!) signature separate.
func EncryptSignAttachment(
publicKey, privateKey, passphrase, fileName string,
plainData []byte,
) (keyPacket, dataPacket, signature []byte, err error) {
var publicKeyRing, privateKeyRing *crypto.KeyRing
var packets *crypto.PGPSplitMessage
var signatureObj *crypto.PGPSignature
var binMessage = crypto.NewPlainMessage(plainData)
if publicKeyRing, err = pgp.BuildKeyRingArmored(publicKey); err != nil {
return nil, nil, nil, err
}
if privateKeyRing, err = pgp.BuildKeyRingArmored(privateKey); err != nil {
return nil, nil, nil, err
}
if err = privateKeyRing.UnlockWithPassphrase(passphrase); err != nil {
return nil, nil, nil, err
}
if packets, err = publicKeyRing.EncryptAttachment(binMessage, fileName); err != nil {
return nil, nil, nil, err
}
if signatureObj, err = privateKeyRing.SignDetached(binMessage); err != nil {
return nil, nil, nil, err
}
return packets.GetKeyPacket(), packets.GetDataPacket(), signatureObj.GetBinary(), nil
}
// DecryptVerifyAttachment decrypts and verifies an attachment split into the keyPacket, dataPacket
// and an armored (!) signature, given a publicKey, and a privateKey with its passphrase.
// Returns the plain data or an error on signature verification failure.
func DecryptVerifyAttachment(
publicKey, privateKey, passphrase string,
keyPacket, dataPacket []byte,
armoredSignature string,
) (plainData []byte, err error) {
var publicKeyRing, privateKeyRing *crypto.KeyRing
var detachedSignature *crypto.PGPSignature
var message *crypto.PlainMessage
var verification *crypto.Verification
var packets = crypto.NewPGPSplitMessage(keyPacket, dataPacket)
if publicKeyRing, err = pgp.BuildKeyRingArmored(publicKey); err != nil {
return nil, err
}
if privateKeyRing, err = pgp.BuildKeyRingArmored(privateKey); err != nil {
return nil, err
}
if err = privateKeyRing.UnlockWithPassphrase(passphrase); err != nil {
return nil, err
}
if detachedSignature, err = crypto.NewPGPSignatureFromArmored(armoredSignature); err != nil {
return nil, err
}
if message, err = privateKeyRing.DecryptAttachment(packets); err != nil {
return nil, err
}
if verification, err = publicKeyRing.VerifyDetached(message, detachedSignature, pgp.GetUnixTime()); err != nil {
return nil, errors.New("gopenpgp: unable to verify attachment")
}
if !verification.IsValid() {
return nil, errors.New("gopenpgp: unable to verify attachment")
}
return message.GetBinary(), nil
}

131
helper/helper_test.go Normal file
View file

@ -0,0 +1,131 @@
package helper
import (
"testing"
"github.com/ProtonMail/gopenpgp/crypto"
"github.com/stretchr/testify/assert"
)
func TestAESEncryption(t *testing.T) {
var plaintext = "Symmetric secret"
var passphrase = "passphrase"
ciphertext, err := EncryptMessageWithToken(passphrase, plaintext)
if err != nil {
t.Fatal("Expected no error when encrypting, got:", err)
}
_, err = DecryptMessageWithToken("Wrong passphrase", ciphertext)
assert.EqualError(t, err, "gopenpgp: wrong password in symmetric decryption")
decrypted, err := DecryptMessageWithToken(passphrase, ciphertext)
if err != nil {
t.Fatal("Expected no error when decrypting, got:", err)
}
assert.Exactly(t, plaintext, decrypted)
}
func TestArmoredTextMessageEncryption(t *testing.T) {
var plaintext = "Secret message"
armored, err := EncryptMessageArmored(readTestFile("keyring_publicKey", false), plaintext)
if err != nil {
t.Fatal("Expected no error when encrypting, got:", err)
}
assert.Exactly(t, true, pgp.IsPGPMessage(armored))
decrypted, err := DecryptMessageArmored(
readTestFile("keyring_privateKey", false),
testMailboxPassword, // Password defined in base_test
armored,
)
if err != nil {
t.Fatal("Expected no error when decrypting, got:", err)
}
assert.Exactly(t, plaintext, decrypted)
}
func TestArmoredTextMessageEncryptionVerification(t *testing.T) {
var plaintext = "Secret message"
armored, err := EncryptSignMessageArmored(
readTestFile("keyring_publicKey", false),
readTestFile("keyring_privateKey", false),
testMailboxPassword, // Password defined in base_test
plaintext,
)
if err != nil {
t.Fatal("Expected no error when encrypting, got:", err)
}
assert.Exactly(t, true, pgp.IsPGPMessage(armored))
_, err = DecryptVerifyMessageArmored(
readTestFile("mime_publicKey", false), // Wrong public key
readTestFile("keyring_privateKey", false),
testMailboxPassword, // Password defined in base_test
armored,
)
assert.EqualError(t, err, "gopenpgp: unable to verify message")
decrypted, err := DecryptVerifyMessageArmored(
readTestFile("keyring_publicKey", false),
readTestFile("keyring_privateKey", false),
testMailboxPassword, // Password defined in base_test
armored,
)
if err != nil {
t.Fatal("Expected no error when decrypting, got:", err)
}
assert.Exactly(t, plaintext, decrypted)
}
func TestAttachmentEncryptionVerification(t *testing.T) {
var attachment = []byte("Secret file\r\nRoot password:hunter2")
keyPacket, dataPacket, signature, err := EncryptSignAttachment(
readTestFile("keyring_publicKey", false),
readTestFile("keyring_privateKey", false),
testMailboxPassword, // Password defined in base_test
"password.txt",
attachment,
)
if err != nil {
t.Fatal("Expected no error when encrypting, got:", err)
}
sig := crypto.NewPGPSignature(signature)
armoredSig, err := sig.GetArmored()
if err != nil {
t.Fatal("Expected no error when armoring signature, got:", err)
}
_, err = DecryptVerifyAttachment(
readTestFile("mime_publicKey", false), // Wrong public key
readTestFile("keyring_privateKey", false),
testMailboxPassword, // Password defined in base_test
keyPacket,
dataPacket,
armoredSig,
)
assert.EqualError(t, err, "gopenpgp: unable to verify attachment")
decrypted, err := DecryptVerifyAttachment(
readTestFile("keyring_publicKey", false),
readTestFile("keyring_privateKey", false),
testMailboxPassword, // Password defined in base_test
keyPacket,
dataPacket,
armoredSig,
)
if err != nil {
t.Fatal("Expected no error when decrypting, got:", err)
}
assert.Exactly(t, attachment, decrypted)
}