mirror of
https://github.com/BishopFox/sliver.git
synced 2026-11-08 17:09:51 -10:00
initial refactor, broken tests
This commit is contained in:
42 files changed
+2069
-460
No files matched your search
@@ -5,6 +5,7 @@ go 1.20
|
||||
replace github.com/rsteube/carapace v0.36.3 => github.com/reeflective/carapace v0.25.2-0.20230602202234-e8d757e458ca
|
||||
|
||||
require (
|
||||
filippo.io/age v1.1.1
|
||||
github.com/AlecAivazis/survey/v2 v2.3.6
|
||||
github.com/Binject/binjection v0.0.0-20200705191933-da1a50d7013d
|
||||
github.com/Binject/debug v0.0.0-20210312092933-6277045c2fdf
|
||||
|
||||
@@ -2,6 +2,8 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMT
|
||||
cloud.google.com/go v0.110.0 h1:Zc8gqp3+a9/Eyph2KDmcGaPtbKRIoqq4YTlL4NMD0Ys=
|
||||
cloud.google.com/go/compute v1.18.0 h1:FEigFqoDbys2cvFkZ9Fjq4gnHBP55anJ0yQyau2f9oY=
|
||||
cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY=
|
||||
filippo.io/age v1.1.1 h1:pIpO7l151hCnQ4BdyBujnGP2YlUo0uj6sAVNHGBvXHg=
|
||||
filippo.io/age v1.1.1/go.mod h1:l03SrzDUrBkdBx8+IILdnn2KZysqQdbEBUQ4p3sqEQE=
|
||||
github.com/AlecAivazis/survey/v2 v2.0.5/go.mod h1:WYBhg6f0y/fNYUuesWQc0PKbJcEliGcYHB9sNT3Bg74=
|
||||
github.com/AlecAivazis/survey/v2 v2.3.6 h1:NvTuVHISgTHEHeBFqt6BHOe4Ny/NwGZr7w+F8S9ziyw=
|
||||
github.com/AlecAivazis/survey/v2 v2.3.6/go.mod h1:4AuI9b7RjAR+G7v9+C4YSlX/YL3K3cWNXgWXOhllqvI=
|
||||
|
||||
@@ -26,29 +26,17 @@ import (
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"io"
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
// {{if .Config.Debug}}
|
||||
"log"
|
||||
// {{end}}
|
||||
|
||||
"github.com/pquerna/otp"
|
||||
"github.com/pquerna/otp/totp"
|
||||
"golang.org/x/crypto/chacha20poly1305"
|
||||
"golang.org/x/crypto/nacl/box"
|
||||
)
|
||||
|
||||
var (
|
||||
totpOptions = totp.ValidateOpts{
|
||||
Digits: 8,
|
||||
Algorithm: otp.AlgorithmSHA256,
|
||||
Period: uint(30),
|
||||
Skew: uint(1),
|
||||
}
|
||||
|
||||
// ErrReplayAttack - Replay attack
|
||||
ErrReplayAttack = errors.New("replay attack detected")
|
||||
// ErrDecryptFailed
|
||||
@@ -58,9 +46,9 @@ var (
|
||||
)
|
||||
|
||||
// ECCKeyPair - Holds the public/private key pair
|
||||
type ECCKeyPair struct {
|
||||
Public *[32]byte
|
||||
Private *[32]byte
|
||||
type AgeKeyPair struct {
|
||||
Public string
|
||||
Private string
|
||||
}
|
||||
|
||||
func init() {
|
||||
@@ -74,23 +62,17 @@ func init() {
|
||||
|
||||
const msgSizeLimit = 8 * 1024
|
||||
|
||||
// ECCEncrypt - Encrypt using Nacl Box
|
||||
func ECCEncrypt(recipientPublicKey *[32]byte, senderPrivateKey *[32]byte, plaintext []byte) ([]byte, error) {
|
||||
// AgeEncrypt - Encrypt using Nacl Box
|
||||
func AgeEncrypt(recipientPublicKey string, plaintext []byte) ([]byte, error) {
|
||||
// Arbitrary upper limit for plaintext
|
||||
if msgSizeLimit < len(plaintext) {
|
||||
return nil, errors.New("plaintext too long")
|
||||
}
|
||||
|
||||
var nonce [24]byte
|
||||
if _, err := io.ReadFull(rand.Reader, nonce[:]); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
encrypted := box.Seal(nonce[:], plaintext, &nonce, recipientPublicKey, senderPrivateKey)
|
||||
return encrypted, nil
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// ECCDecrypt - Decrypt using Curve 25519 + ChaCha20Poly1305
|
||||
func ECCDecrypt(senderPublicKey *[32]byte, recipientPrivateKey *[32]byte, ciphertext []byte) ([]byte, error) {
|
||||
// AgeDecrypt - Decrypt using Curve 25519 + ChaCha20Poly1305
|
||||
func AgeDecrypt(recipientPrivateKey string, ciphertext []byte) ([]byte, error) {
|
||||
if len(ciphertext) < 24 {
|
||||
return nil, errors.New("ciphertext too short")
|
||||
}
|
||||
@@ -99,13 +81,7 @@ func ECCDecrypt(senderPublicKey *[32]byte, recipientPrivateKey *[32]byte, cipher
|
||||
return nil, errors.New("ciphertext too long")
|
||||
}
|
||||
|
||||
var decryptNonce [24]byte
|
||||
copy(decryptNonce[:], ciphertext[:24])
|
||||
plaintext, ok := box.Open(nil, ciphertext[24:], &decryptNonce, senderPublicKey, recipientPrivateKey)
|
||||
if !ok {
|
||||
return nil, ErrDecryptFailed
|
||||
}
|
||||
return plaintext, nil
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// RandomKey - Generate random ID of randomIDSize bytes
|
||||
@@ -184,7 +160,7 @@ func (c *CipherContext) Decrypt(msg []byte) ([]byte, error) {
|
||||
return nil, ErrDecryptFailed
|
||||
}
|
||||
ciphertext := msg[minisignRawSigSize:]
|
||||
serverPublicKey, err := DecodeMinisignPublicKey(minisignServerPublicKey)
|
||||
serverPublicKey, err := DecodeMinisignPublicKey(serverMinisignPublicKey)
|
||||
if err != nil {
|
||||
// {{if .Config.Debug}}
|
||||
log.Printf("failed to decode minisign public key: %s", err)
|
||||
@@ -227,35 +203,6 @@ func (c *CipherContext) Encrypt(plaintext []byte) ([]byte, error) {
|
||||
return ciphertext, nil
|
||||
}
|
||||
|
||||
// GetExactOTPCode - Get the OTP code for a specific timestamp
|
||||
func GetExactOTPCode(timestamp time.Time) string {
|
||||
code, _ := totp.GenerateCodeCustom(totpSecret, timestamp, totpOptions)
|
||||
// {{if .Config.Debug}}
|
||||
log.Printf("TOTP Code (%s): %s", timestamp, code)
|
||||
// {{end}}
|
||||
return code
|
||||
}
|
||||
|
||||
// GetOTPCode - Get the current OTP code
|
||||
func GetOTPCode() string {
|
||||
now := time.Now().UTC()
|
||||
code, _ := totp.GenerateCodeCustom(totpSecret, now, totpOptions)
|
||||
// {{if .Config.Debug}}
|
||||
log.Printf("TOTP Code: %s", code)
|
||||
// {{end}}
|
||||
return code
|
||||
}
|
||||
|
||||
// ValidateTOTP - Validate a TOTP code
|
||||
func ValidateTOTP(code string) (bool, error) {
|
||||
now := time.Now().UTC()
|
||||
valid, err := totp.ValidateCustom(code, totpSecret, now, totpOptions)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return valid, nil
|
||||
}
|
||||
|
||||
// rootOnlyVerifyCertificate - Go doesn't provide a method for only skipping hostname validation so
|
||||
// we have to disable all of the certificate validation and re-implement everything.
|
||||
// https://github.com/golang/go/issues/21971
|
||||
|
||||
@@ -20,7 +20,6 @@ package cryptography
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
|
||||
// {{if .Config.Debug}}
|
||||
@@ -29,59 +28,43 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
// ECCPublicKey - The implant's ECC public key
|
||||
ECCPublicKey = "{{.Config.ECCPublicKey}}"
|
||||
// eccPrivateKey - The implant's ECC private key
|
||||
eccPrivateKey = "{{.Config.ECCPrivateKey}}"
|
||||
// eccPublicKeySignature - The implant's public key minisigned'd
|
||||
ECCPublicKeySignature = `{{.Config.ECCPublicKeySignature}}`
|
||||
// eccServerPublicKey - Server's ECC public key
|
||||
eccServerPublicKey = "{{.Config.ECCServerPublicKey}}"
|
||||
// minisignServerPublicKey - The server's minisign public key
|
||||
minisignServerPublicKey = `{{.Config.MinisignServerPublicKey}}`
|
||||
|
||||
// TOTP secret value
|
||||
totpSecret = "{{.OTPSecret}}"
|
||||
// PeerAgePublicKey - The implant's age public key
|
||||
PeerAgePublicKey = "{{.Config.ECCPublicKey}}"
|
||||
// peerPrivateKey - The implant's age private key
|
||||
peerAgePrivateKey = "{{.Config.ECCPrivateKey}}"
|
||||
// PublicKeySignature - The implant's age public key minisigned'd
|
||||
PeerAgePublicKeySignature = `{{.Config.ECCPublicKeySignature}}`
|
||||
// serverPublicKey - Server's ECC public key
|
||||
serverAgePublicKey = "{{.Config.ECCServerPublicKey}}"
|
||||
// serverMinisignPublicKey - The server's minisign public key
|
||||
serverMinisignPublicKey = `{{.Config.MinisignServerPublicKey}}`
|
||||
|
||||
// ErrInvalidPeerKey - Peer to peer key exchange failed
|
||||
ErrInvalidPeerKey = errors.New("invalid peer key")
|
||||
)
|
||||
|
||||
// {{if .Config.Debug}} - Used for unit tests, remove from normal builds where these values are set at compile-time
|
||||
func SetSecrets(newEccPublicKey, newEccPrivateKey, newEccPublicKeySignature, newEccServerPublicKey, newTotpSecret, newMinisignServerPublicKey string) {
|
||||
ECCPublicKey = newEccPublicKey
|
||||
eccPrivateKey = newEccPrivateKey
|
||||
ECCPublicKeySignature = newEccPublicKeySignature
|
||||
eccServerPublicKey = newEccServerPublicKey
|
||||
totpSecret = newTotpSecret
|
||||
minisignServerPublicKey = newMinisignServerPublicKey
|
||||
func SetSecrets(peerPublicKey, peerPrivateKey, peerPublicKeySignature, serverPublicKey, minisignServerPublicKey string) {
|
||||
PeerAgePublicKey = peerPublicKey
|
||||
peerAgePrivateKey = peerPrivateKey
|
||||
PeerAgePublicKeySignature = peerPublicKeySignature
|
||||
serverAgePublicKey = serverPublicKey
|
||||
serverMinisignPublicKey = minisignServerPublicKey
|
||||
}
|
||||
|
||||
// {{end}}
|
||||
|
||||
// GetECCKeyPair - Get the implant's key pair
|
||||
func GetECCKeyPair() *ECCKeyPair {
|
||||
publicRaw, err := base64.RawStdEncoding.DecodeString(ECCPublicKey)
|
||||
if err != nil {
|
||||
panic("no public key")
|
||||
}
|
||||
var public [32]byte
|
||||
copy(public[:], publicRaw)
|
||||
privateRaw, err := base64.RawStdEncoding.DecodeString(eccPrivateKey)
|
||||
if err != nil {
|
||||
panic("no private key")
|
||||
}
|
||||
var private [32]byte
|
||||
copy(private[:], privateRaw)
|
||||
return &ECCKeyPair{
|
||||
Public: &public,
|
||||
Private: &private,
|
||||
// GetAgeKeyPair - Get the implant's key pair
|
||||
func GetAgeKeyPair() *AgeKeyPair {
|
||||
return &AgeKeyPair{
|
||||
Public: PeerAgePublicKey,
|
||||
Private: peerAgePrivateKey,
|
||||
}
|
||||
}
|
||||
|
||||
// MinisignVerify - Verify a minisign signature
|
||||
func MinisignVerify(message []byte, signature string) bool {
|
||||
serverPublicKey, err := DecodeMinisignPublicKey(minisignServerPublicKey)
|
||||
serverPublicKey, err := DecodeMinisignPublicKey(serverMinisignPublicKey)
|
||||
if err != nil {
|
||||
// {{if .Config.Debug}}
|
||||
log.Printf("failed to decode minisign public key: %s", err)
|
||||
@@ -109,60 +92,54 @@ func MinisignVerify(message []byte, signature string) bool {
|
||||
}
|
||||
|
||||
// GetServerECCPublicKey - Get the decoded server public key
|
||||
func GetServerECCPublicKey() *[32]byte {
|
||||
publicRaw, err := base64.RawStdEncoding.DecodeString(eccServerPublicKey)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
var public [32]byte
|
||||
copy(public[:], publicRaw)
|
||||
return &public
|
||||
func GetServerECCPublicKey() string {
|
||||
return serverAgePublicKey
|
||||
}
|
||||
|
||||
// ECCEncryptToServer - Encrypt using the server's public key
|
||||
func ECCEncryptToServer(plaintext []byte) ([]byte, error) {
|
||||
// AgeEncryptToServer - Encrypt using the server's public key
|
||||
func AgeEncryptToServer(plaintext []byte) ([]byte, error) {
|
||||
recipientPublicKey := GetServerECCPublicKey()
|
||||
if recipientPublicKey == nil {
|
||||
if recipientPublicKey == "" {
|
||||
panic("no server public key")
|
||||
}
|
||||
keyPair := GetECCKeyPair()
|
||||
ciphertext, err := ECCEncrypt(recipientPublicKey, keyPair.Private, plaintext)
|
||||
keyPair := GetAgeKeyPair()
|
||||
ciphertext, err := AgeEncrypt(recipientPublicKey, plaintext)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
digest := sha256.Sum256((*keyPair.Public)[:])
|
||||
digest := sha256.Sum256([]byte(keyPair.Public))
|
||||
msg := make([]byte, 32+len(ciphertext))
|
||||
copy(msg, digest[:])
|
||||
copy(msg[32:], ciphertext)
|
||||
return msg, nil
|
||||
}
|
||||
|
||||
// ECCEncryptToPeer - Encrypt using the peer's public key
|
||||
func ECCEncryptToPeer(recipientPublicKey []byte, recipientPublicKeySig string, plaintext []byte) ([]byte, error) {
|
||||
// AgeEncryptToPeer - Encrypt using the peer's public key
|
||||
func AgeEncryptToPeer(recipientPublicKey []byte, recipientPublicKeySig string, plaintext []byte) ([]byte, error) {
|
||||
valid := MinisignVerify(recipientPublicKey, recipientPublicKeySig)
|
||||
if !valid {
|
||||
return nil, ErrInvalidPeerKey
|
||||
}
|
||||
var peerPublicKey [32]byte
|
||||
copy(peerPublicKey[:], recipientPublicKey)
|
||||
keyPair := GetECCKeyPair()
|
||||
ciphertext, err := ECCEncrypt(&peerPublicKey, keyPair.Private, plaintext)
|
||||
keyPair := GetAgeKeyPair()
|
||||
ciphertext, err := AgeEncrypt(keyPair.Public, plaintext)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return ciphertext, nil
|
||||
}
|
||||
|
||||
// ECCDecryptFromPeer - Decrypt a message from a peer
|
||||
func ECCDecryptFromPeer(senderPublicKey []byte, senderPublicKeySig string, ciphertext []byte) ([]byte, error) {
|
||||
// AgeDecryptFromPeer - Decrypt a message from a peer
|
||||
func AgeDecryptFromPeer(senderPublicKey []byte, senderPublicKeySig string, ciphertext []byte) ([]byte, error) {
|
||||
valid := MinisignVerify(senderPublicKey, senderPublicKeySig)
|
||||
if !valid {
|
||||
return nil, ErrInvalidPeerKey
|
||||
}
|
||||
var peerPublicKey [32]byte
|
||||
copy(peerPublicKey[:], senderPublicKey)
|
||||
keyPair := GetECCKeyPair()
|
||||
plaintext, err := ECCDecrypt(&peerPublicKey, keyPair.Private, ciphertext)
|
||||
keyPair := GetAgeKeyPair()
|
||||
plaintext, err := AgeDecrypt(keyPair.Private, ciphertext)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -21,7 +21,6 @@ package pivots
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"io"
|
||||
@@ -411,17 +410,16 @@ func (p *NetConnPivot) peerKeyExchange() error {
|
||||
p.downstreamPeerID = peerHello.PeerID
|
||||
sessionKey := cryptography.RandomKey()
|
||||
p.cipherCtx = cryptography.NewCipherContext(sessionKey)
|
||||
ciphertext, err := cryptography.ECCEncryptToPeer(peerHello.PublicKey, peerHello.PublicKeySignature, sessionKey[:])
|
||||
ciphertext, err := cryptography.AgeEncryptToPeer(peerHello.PublicKey, peerHello.PublicKeySignature, sessionKey[:])
|
||||
if err != nil {
|
||||
// {{if .Config.Debug}}
|
||||
log.Printf("[pivot] peer encryption failure: %s", err)
|
||||
// {{end}}
|
||||
return ErrFailedKeyExchange
|
||||
}
|
||||
publicKeyRaw, _ := base64.RawStdEncoding.DecodeString(cryptography.ECCPublicKey)
|
||||
peerResponse, _ := proto.Marshal(&pb.PivotHello{
|
||||
PublicKey: publicKeyRaw,
|
||||
PublicKeySignature: cryptography.ECCPublicKeySignature,
|
||||
PublicKey: []byte(cryptography.PeerAgePublicKey),
|
||||
PublicKeySignature: cryptography.PeerAgePublicKeySignature,
|
||||
SessionKey: ciphertext,
|
||||
})
|
||||
p.conn.SetWriteDeadline(time.Now().Add(tcpPivotWriteDeadline))
|
||||
|
||||
@@ -314,7 +314,7 @@ func (s *SliverDNSClient) SessionInit() error {
|
||||
// Key agreement with server
|
||||
sKey := cryptography.RandomKey()
|
||||
s.cipherCtx = cryptography.NewCipherContext(sKey)
|
||||
initData, err := cryptography.ECCEncryptToServer(sKey[:])
|
||||
initData, err := cryptography.AgeEncryptToServer(sKey[:])
|
||||
if err != nil {
|
||||
// {{if .Config.Debug}}
|
||||
log.Printf("[dns] failed to encrypt init msg %v", err)
|
||||
@@ -815,14 +815,9 @@ func (s *SliverDNSClient) pollMsg(meta *ResolverMetadata) (string, error) {
|
||||
}
|
||||
|
||||
func (s *SliverDNSClient) otpMsg() (string, error) {
|
||||
otpCode := cryptography.GetOTPCode()
|
||||
otp, err := strconv.Atoi(otpCode)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
otpMsg := &dnspb.DNSMessage{
|
||||
Type: dnspb.DNSMessageType_TOTP,
|
||||
ID: uint32(otp), // Take advantage of the variable length encoding
|
||||
ID: uint32(0), // Take advantage of the variable length encoding
|
||||
}
|
||||
data, err := proto.Marshal(otpMsg)
|
||||
if err != nil {
|
||||
|
||||
@@ -27,6 +27,10 @@ import (
|
||||
"github.com/bishopfox/sliver/implant/sliver/transports/httpclient/drivers/win/wininet"
|
||||
)
|
||||
|
||||
var (
|
||||
wininetDriver = "wininet"
|
||||
)
|
||||
|
||||
// GetHTTPDriver - Get an instance of the specified HTTP driver
|
||||
func GetHTTPDriver(origin string, secure bool, opts *HTTPOptions) (HTTPDriver, error) {
|
||||
switch opts.Driver {
|
||||
|
||||
@@ -46,8 +46,7 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
wininetDriver = "wininet"
|
||||
goHTTPDriver = "go"
|
||||
goHTTPDriver = "go"
|
||||
|
||||
userAgent = "{{GenerateUserAgent}}"
|
||||
nonceQueryArgs = "{{.HTTPC2ImplantConfig.NonceQueryArgs}}" // "abcdefghijklmnopqrstuvwxyz"
|
||||
@@ -179,7 +178,7 @@ func (s *SliverHTTPClient) SessionInit() error {
|
||||
httpSessionInit := &pb.HTTPSessionInit{Key: sKey[:]}
|
||||
data, _ := proto.Marshal(httpSessionInit)
|
||||
|
||||
encryptedSessionInit, err := cryptography.ECCEncryptToServer(data)
|
||||
encryptedSessionInit, err := cryptography.AgeEncryptToServer(data)
|
||||
if err != nil {
|
||||
// {{if .Config.Debug}}
|
||||
log.Printf("Nacl encrypt failed %v", err)
|
||||
@@ -360,9 +359,7 @@ func (s *SliverHTTPClient) establishSessionID(sessionInit []byte) error {
|
||||
|
||||
uri := s.startSessionURL()
|
||||
s.NonceQueryArgument(uri, nonce)
|
||||
timestamp := time.Now().UTC().Add(TimeDelta)
|
||||
otpCode := cryptography.GetExactOTPCode(timestamp)
|
||||
s.OTPQueryArgument(uri, otpCode)
|
||||
|
||||
req := s.newHTTPRequest(http.MethodPost, uri, reqBody)
|
||||
// {{if .Config.Debug}}
|
||||
log.Printf("[http] POST -> %s (%d bytes)", uri, len(sessionInit))
|
||||
|
||||
@@ -20,7 +20,6 @@ package pivotclients
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"io"
|
||||
@@ -73,17 +72,10 @@ func (p *NetConnPivotClient) KeyExchange() error {
|
||||
}
|
||||
|
||||
func (p *NetConnPivotClient) peerKeyExchange() error {
|
||||
publicKey, err := base64.RawStdEncoding.DecodeString(cryptography.ECCPublicKey)
|
||||
if err != nil {
|
||||
// {{if .Config.Debug}}
|
||||
log.Printf("[pivot] Error decoding public key: %v", err)
|
||||
// {{end}}
|
||||
return err
|
||||
}
|
||||
pivotHello, _ := proto.Marshal(&pb.PivotHello{
|
||||
PublicKey: publicKey,
|
||||
PublicKey: []byte(cryptography.PeerAgePublicKey),
|
||||
PeerID: pivots.MyPeerID,
|
||||
PublicKeySignature: cryptography.ECCPublicKeySignature,
|
||||
PublicKeySignature: cryptography.PeerAgePublicKeySignature,
|
||||
})
|
||||
|
||||
// Enforce deadlines on the key exchange
|
||||
@@ -108,7 +100,7 @@ func (p *NetConnPivotClient) peerKeyExchange() error {
|
||||
// {{end}}
|
||||
return err
|
||||
}
|
||||
peerSessionKey, err := cryptography.ECCDecryptFromPeer(peerHello.PublicKey, peerHello.PublicKeySignature, peerHello.SessionKey)
|
||||
peerSessionKey, err := cryptography.AgeDecryptFromPeer(peerHello.PublicKey, peerHello.PublicKeySignature, peerHello.SessionKey)
|
||||
if err != nil || len(peerSessionKey) != 32 {
|
||||
// {{if .Config.Debug}}
|
||||
log.Printf("[pivot] Error decrypting session key: %v", err)
|
||||
@@ -124,7 +116,7 @@ func (p *NetConnPivotClient) peerKeyExchange() error {
|
||||
func (p *NetConnPivotClient) serverKeyExchange() error {
|
||||
serverSessionKey := cryptography.RandomKey()
|
||||
p.serverCipherCtx = cryptography.NewCipherContext(serverSessionKey)
|
||||
ciphertext, err := cryptography.ECCEncryptToServer(serverSessionKey[:])
|
||||
ciphertext, err := cryptography.AgeEncryptToServer(serverSessionKey[:])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -26,10 +26,6 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
bufSize = 1024
|
||||
)
|
||||
|
||||
var (
|
||||
ErrFailedWrite = errors.New("write failed")
|
||||
|
||||
|
||||
@@ -178,11 +178,11 @@ func handleBuildEvent(externalBuilder *clientpb.Builder, event *clientpb.Event,
|
||||
case clientpb.OutputFormat_SERVICE:
|
||||
fallthrough
|
||||
case clientpb.OutputFormat_EXECUTABLE:
|
||||
fPath, err = generate.SliverExecutable(extConfig.Config.Name, extConfig.OTPSecret, extModel, false)
|
||||
fPath, err = generate.SliverExecutable(extConfig.Config.Name, extModel, false)
|
||||
case clientpb.OutputFormat_SHARED_LIB:
|
||||
fPath, err = generate.SliverSharedLibrary(extConfig.Config.Name, extConfig.OTPSecret, extModel, false)
|
||||
fPath, err = generate.SliverSharedLibrary(extConfig.Config.Name, extModel, false)
|
||||
case clientpb.OutputFormat_SHELLCODE:
|
||||
fPath, err = generate.SliverShellcode(extConfig.Config.Name, extConfig.OTPSecret, extModel, false)
|
||||
fPath, err = generate.SliverShellcode(extConfig.Config.Name, extModel, false)
|
||||
default:
|
||||
builderLog.Errorf("invalid output format: %s", extConfig.Config.Format)
|
||||
rpc.BuilderTrigger(context.Background(), &clientpb.Event{
|
||||
|
||||
+6
-22
@@ -19,8 +19,6 @@ package c2
|
||||
*/
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
insecureRand "math/rand"
|
||||
"os"
|
||||
"testing"
|
||||
@@ -34,8 +32,7 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
serverECCKeyPair *cryptography.ECCKeyPair
|
||||
implantECCKeyPair *cryptography.ECCKeyPair
|
||||
serverAgeKeyPair *cryptography.AgeKeyPair
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
@@ -58,29 +55,16 @@ func TestMain(m *testing.M) {
|
||||
func setup() *models.ImplantConfig {
|
||||
var err error
|
||||
certs.SetupCAs()
|
||||
serverECCKeyPair = cryptography.ECCServerKeyPair()
|
||||
implantECCKeyPair, err = cryptography.RandomECCKeyPair()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
totpSecret, err := cryptography.TOTPServerSecret()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
serverAgeKeyPair = cryptography.AgeServerKeyPair()
|
||||
implantCrypto.SetSecrets(
|
||||
implantECCKeyPair.PublicBase64(),
|
||||
implantECCKeyPair.PrivateBase64(),
|
||||
"",
|
||||
serverECCKeyPair.PublicBase64(),
|
||||
totpSecret,
|
||||
"",
|
||||
"",
|
||||
serverAgeKeyPair.Public,
|
||||
cryptography.MinisignServerPublicKey(),
|
||||
)
|
||||
digest := sha256.Sum256(implantECCKeyPair.Public[:])
|
||||
implantConfig := &models.ImplantConfig{
|
||||
ECCPublicKey: implantECCKeyPair.PublicBase64(),
|
||||
ECCPrivateKey: implantECCKeyPair.PrivateBase64(),
|
||||
ECCPublicKeyDigest: hex.EncodeToString(digest[:]),
|
||||
ECCServerPublicKey: serverECCKeyPair.PublicBase64(),
|
||||
ECCServerPublicKey: serverAgeKeyPair.Public,
|
||||
}
|
||||
err = db.Session().Create(implantConfig).Error
|
||||
if err != nil {
|
||||
|
||||
+2
-13
@@ -460,17 +460,6 @@ func (s *SliverDNSServer) refusedErrorResp(req *dns.Msg) *dns.Msg {
|
||||
// ---------------------------
|
||||
func (s *SliverDNSServer) handleTOTP(domain string, msg *dnspb.DNSMessage, req *dns.Msg) *dns.Msg {
|
||||
dnsLog.Debugf("[dns] totp request: %v", msg)
|
||||
totpCode := fmt.Sprintf("%08d", msg.ID)
|
||||
if s.EnforceOTP {
|
||||
valid, err := cryptography.ValidateTOTP(totpCode)
|
||||
if err != nil || !valid {
|
||||
dnsLog.Warnf("totp request invalid (%v)", err)
|
||||
return s.nameErrorResp(req)
|
||||
}
|
||||
dnsLog.Debugf("[dns] totp request valid")
|
||||
} else {
|
||||
dnsLog.Warn("[dns] totp validation is disabled")
|
||||
}
|
||||
|
||||
dnsSessionID := dnsSessionID()
|
||||
dnsLog.Debugf("[dns] Assigned new dns session id = %d", dnsSessionID&sessionIDBitMask)
|
||||
@@ -529,8 +518,8 @@ func (s *SliverDNSServer) handleDNSSessionInit(domain string, msg *dnspb.DNSMess
|
||||
}
|
||||
var senderPublicKey [32]byte
|
||||
copy(senderPublicKey[:], publicKey)
|
||||
serverKeyPair := cryptography.ECCServerKeyPair()
|
||||
sessionInit, err := cryptography.ECCDecrypt(&senderPublicKey, serverKeyPair.Private, msg.Data[32:])
|
||||
serverKeyPair := cryptography.AgeServerKeyPair()
|
||||
sessionInit, err := cryptography.AgeDecrypt(serverKeyPair.Private, msg.Data[32:])
|
||||
if err != nil {
|
||||
dnsLog.Errorf("[session init] error decrypting session init data: %s", err)
|
||||
return s.refusedErrorResp(req)
|
||||
|
||||
+4
-29
@@ -353,7 +353,7 @@ func (s *SliverHTTPC2) router() *mux.Router {
|
||||
router.HandleFunc(
|
||||
fmt.Sprintf("/{rpath:.*\\.%s$}", c2Config.ImplantConfig.StartSessionFileExt),
|
||||
s.startSessionHandler,
|
||||
).MatcherFunc(s.filterOTP).MatcherFunc(s.filterNonce).Methods(http.MethodGet, http.MethodPost)
|
||||
).MatcherFunc(s.filterNonce).Methods(http.MethodGet, http.MethodPost)
|
||||
|
||||
// Session Handler
|
||||
router.HandleFunc(
|
||||
@@ -379,7 +379,7 @@ func (s *SliverHTTPC2) router() *mux.Router {
|
||||
router.HandleFunc(
|
||||
fmt.Sprintf("/{rpath:.*\\.%s[/]{0,1}.*$}", c2Config.ImplantConfig.StagerFileExt),
|
||||
s.stagerHandler,
|
||||
).MatcherFunc(s.filterOTP).Methods(http.MethodGet)
|
||||
).Methods(http.MethodGet)
|
||||
|
||||
// Default handler returns static content or 404s
|
||||
httpLog.Debugf("No pattern matches for request uri")
|
||||
@@ -405,31 +405,6 @@ func (s *SliverHTTPC2) filterNonce(req *http.Request, rm *mux.RouteMatch) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (s *SliverHTTPC2) filterOTP(req *http.Request, rm *mux.RouteMatch) bool {
|
||||
if s.ServerConf.EnforceOTP {
|
||||
httpLog.Debug("Checking for valid OTP code ...")
|
||||
otpCode, err := getOTPFromURL(req.URL)
|
||||
if err != nil {
|
||||
httpLog.Warnf("Failed to validate OTP: %s", err)
|
||||
return false
|
||||
}
|
||||
valid, err := cryptography.ValidateTOTP(otpCode)
|
||||
if err != nil {
|
||||
httpLog.Warnf("Failed to validate OTP: %s", err)
|
||||
return false
|
||||
}
|
||||
if valid {
|
||||
httpLog.Debug("OTP code is valid")
|
||||
return true
|
||||
}
|
||||
httpLog.Debugf("OTP code (%s) is invalid", otpCode)
|
||||
return false
|
||||
} else {
|
||||
httpLog.Debug("OTP enforcement is disabled")
|
||||
return true // OTP enforcement is disabled
|
||||
}
|
||||
}
|
||||
|
||||
func getNonceFromURL(reqURL *url.URL) (uint64, error) {
|
||||
qNonce := ""
|
||||
for arg, values := range reqURL.Query() {
|
||||
@@ -578,8 +553,8 @@ func (s *SliverHTTPC2) startSessionHandler(resp http.ResponseWriter, req *http.R
|
||||
var senderPublicKey [32]byte
|
||||
copy(senderPublicKey[:], publicKey)
|
||||
|
||||
serverKeyPair := cryptography.ECCServerKeyPair()
|
||||
sessionInitData, err := cryptography.ECCDecrypt(&senderPublicKey, serverKeyPair.Private, data[32:])
|
||||
serverKeyPair := cryptography.AgeServerKeyPair()
|
||||
sessionInitData, err := cryptography.AgeDecrypt(serverKeyPair.Private, data[32:])
|
||||
if err != nil {
|
||||
httpLog.Error("ECC decryption failed")
|
||||
s.defaultHandler(resp, req)
|
||||
|
||||
@@ -59,13 +59,12 @@ func TestStartSessionHandler(t *testing.T) {
|
||||
}
|
||||
nonce, encoder := implantEncoders.RandomEncoder(0)
|
||||
testURL := client.NonceQueryArgument(baseURL, nonce)
|
||||
testURL = client.OTPQueryArgument(testURL, implantCrypto.GetOTPCode())
|
||||
|
||||
// Generate key exchange request
|
||||
sKey := cryptography.RandomKey()
|
||||
httpSessionInit := &sliverpb.HTTPSessionInit{Key: sKey[:]}
|
||||
data, _ := proto.Marshal(httpSessionInit)
|
||||
encryptedSessionInit, err := implantCrypto.ECCEncryptToServer(data)
|
||||
encryptedSessionInit, err := implantCrypto.AgeEncryptToServer(data)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to encrypt session init %s", err)
|
||||
}
|
||||
|
||||
+1
-2
@@ -123,8 +123,7 @@ var rootCmd = &cobra.Command{
|
||||
assets.Setup(false, true)
|
||||
certs.SetupCAs()
|
||||
certs.SetupWGKeys()
|
||||
cryptography.ECCServerKeyPair()
|
||||
cryptography.TOTPServerSecret()
|
||||
cryptography.AgeServerKeyPair()
|
||||
cryptography.MinisignServerPrivateKey()
|
||||
|
||||
serverConfig := configs.GetServerConfig()
|
||||
|
||||
@@ -51,8 +51,7 @@ var daemonCmd = &cobra.Command{
|
||||
assets.Setup(force, false)
|
||||
certs.SetupCAs()
|
||||
certs.SetupWGKeys()
|
||||
cryptography.ECCServerKeyPair()
|
||||
cryptography.TOTPServerSecret()
|
||||
cryptography.AgeServerKeyPair()
|
||||
cryptography.MinisignServerPrivateKey()
|
||||
|
||||
serverConfig := configs.GetServerConfig()
|
||||
|
||||
@@ -33,23 +33,16 @@ import (
|
||||
"errors"
|
||||
"io"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"filippo.io/age"
|
||||
"github.com/bishopfox/sliver/server/cryptography/minisign"
|
||||
"github.com/bishopfox/sliver/server/db"
|
||||
"github.com/bishopfox/sliver/util/encoders"
|
||||
"github.com/pquerna/otp"
|
||||
"github.com/pquerna/otp/totp"
|
||||
"golang.org/x/crypto/chacha20poly1305"
|
||||
"golang.org/x/crypto/nacl/box"
|
||||
)
|
||||
|
||||
const (
|
||||
// TOTPDigits - Number of digits in the TOTP
|
||||
TOTPDigits = 8
|
||||
TOTPPeriod = uint(30)
|
||||
TOTPSecretKey = "server.totp"
|
||||
ServerECCKeyPairKey = "server.ecc"
|
||||
ServerECCKeyPairKey = "server.age"
|
||||
serverMinisignPrivateKey = "server.minisign"
|
||||
)
|
||||
|
||||
@@ -93,48 +86,69 @@ func KeyFromBytes(data []byte) ([chacha20poly1305.KeySize]byte, error) {
|
||||
return key, nil
|
||||
}
|
||||
|
||||
// ECCKeyPair - Holds the public/private key pair
|
||||
type ECCKeyPair struct {
|
||||
Public *[32]byte `json:"public"`
|
||||
Private *[32]byte `json:"private"`
|
||||
// AgeKeyPair - Holds the public/private key pair
|
||||
type AgeKeyPair struct {
|
||||
Public string `json:"public"`
|
||||
Private string `json:"private"`
|
||||
}
|
||||
|
||||
// PublicBase64 - Base64 encoded public key
|
||||
func (e *ECCKeyPair) PublicBase64() string {
|
||||
return base64.RawStdEncoding.EncodeToString(e.Public[:])
|
||||
// PublicKey - Return the parsed public key
|
||||
func (e *AgeKeyPair) PublicKey() *age.X25519Recipient {
|
||||
recipient, _ := age.ParseX25519Recipient(e.Public)
|
||||
return recipient
|
||||
}
|
||||
|
||||
// PrivateBase64 - Base64 encoded private key
|
||||
func (e *ECCKeyPair) PrivateBase64() string {
|
||||
return base64.RawStdEncoding.EncodeToString(e.Private[:])
|
||||
func (e *AgeKeyPair) PrivateKey() string {
|
||||
return e.Private
|
||||
}
|
||||
|
||||
// RandomECCKeyPair - Generate a random Curve 25519 key pair
|
||||
func RandomECCKeyPair() (*ECCKeyPair, error) {
|
||||
public, private, err := box.GenerateKey(rand.Reader)
|
||||
// RandomAgeKeyPair - Generate a random Curve 25519 key pair
|
||||
func RandomAgeKeyPair() (*AgeKeyPair, error) {
|
||||
k, err := age.GenerateX25519Identity()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &ECCKeyPair{Public: public, Private: private}, nil
|
||||
return &AgeKeyPair{
|
||||
Public: k.Recipient().String(),
|
||||
Private: k.String(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ECCEncrypt - Encrypt using Nacl Box
|
||||
func ECCEncrypt(recipientPublicKey *[32]byte, senderPrivateKey *[32]byte, plaintext []byte) ([]byte, error) {
|
||||
var nonce [24]byte
|
||||
if _, err := io.ReadFull(rand.Reader, nonce[:]); err != nil {
|
||||
// AgeEncrypt - Encrypt using Nacl Box
|
||||
func AgeEncrypt(recipientPublicKey string, plaintext []byte) ([]byte, error) {
|
||||
recipient, err := age.ParseX25519Recipient(recipientPublicKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
encrypted := box.Seal(nonce[:], plaintext, &nonce, recipientPublicKey, senderPrivateKey)
|
||||
return encrypted, nil
|
||||
buf := bytes.NewBuffer([]byte{})
|
||||
stream, err := age.Encrypt(buf, recipient)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, err := stream.Write(plaintext); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := stream.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
// ECCDecrypt - Decrypt using Curve 25519 + ChaCha20Poly1305
|
||||
func ECCDecrypt(senderPublicKey *[32]byte, recipientPrivateKey *[32]byte, ciphertext []byte) ([]byte, error) {
|
||||
var decryptNonce [24]byte
|
||||
copy(decryptNonce[:], ciphertext[:24])
|
||||
plaintext, ok := box.Open(nil, ciphertext[24:], &decryptNonce, senderPublicKey, recipientPrivateKey)
|
||||
if !ok {
|
||||
return nil, ErrDecryptFailed
|
||||
// AgeDecrypt - Decrypt using Curve 25519 + ChaCha20Poly1305
|
||||
func AgeDecrypt(recipientPrivateKey string, ciphertext []byte) ([]byte, error) {
|
||||
identity, err := age.ParseX25519Identity(recipientPrivateKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
buf := bytes.NewBuffer(ciphertext)
|
||||
stream, err := age.Decrypt(buf, identity)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
plaintext, err := io.ReadAll(stream)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return plaintext, nil
|
||||
}
|
||||
@@ -223,16 +237,6 @@ func (c *CipherContext) Encrypt(plaintext []byte) ([]byte, error) {
|
||||
return append(rawSig, ciphertext...), nil
|
||||
}
|
||||
|
||||
// TOTPOptions - Customized totp validation options
|
||||
func TOTPOptions() totp.ValidateOpts {
|
||||
return totp.ValidateOpts{
|
||||
Digits: TOTPDigits,
|
||||
Algorithm: otp.AlgorithmSHA256,
|
||||
Period: TOTPPeriod,
|
||||
Skew: uint(1),
|
||||
}
|
||||
}
|
||||
|
||||
// serverSignRawBuf - Sign a buffer with the server's minisign private key
|
||||
func serverSignRawBuf(buf []byte) []byte {
|
||||
privateKey := MinisignServerPrivateKey()
|
||||
@@ -240,8 +244,8 @@ func serverSignRawBuf(buf []byte) []byte {
|
||||
return rawSig[:]
|
||||
}
|
||||
|
||||
// ECCServerKeyPair - Get teh server's ECC key pair
|
||||
func ECCServerKeyPair() *ECCKeyPair {
|
||||
// AgeServerKeyPair - Get teh server's ECC key pair
|
||||
func AgeServerKeyPair() *AgeKeyPair {
|
||||
data, err := db.GetKeyValue(ServerECCKeyPairKey)
|
||||
if err == db.ErrRecordNotFound {
|
||||
keyPair, err := generateServerECCKeyPair()
|
||||
@@ -250,7 +254,7 @@ func ECCServerKeyPair() *ECCKeyPair {
|
||||
}
|
||||
return keyPair
|
||||
}
|
||||
keyPair := &ECCKeyPair{}
|
||||
keyPair := &AgeKeyPair{}
|
||||
err = json.Unmarshal([]byte(data), keyPair)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
@@ -258,8 +262,8 @@ func ECCServerKeyPair() *ECCKeyPair {
|
||||
return keyPair
|
||||
}
|
||||
|
||||
func generateServerECCKeyPair() (*ECCKeyPair, error) {
|
||||
keyPair, err := RandomECCKeyPair()
|
||||
func generateServerECCKeyPair() (*AgeKeyPair, error) {
|
||||
keyPair, err := RandomAgeKeyPair()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -271,47 +275,6 @@ func generateServerECCKeyPair() (*ECCKeyPair, error) {
|
||||
return keyPair, err
|
||||
}
|
||||
|
||||
// TOTPServerSecret - Get the server-wide totp secret value, the goal of the totp
|
||||
// is for the implant to prove it was generated by this server. To that end we simply
|
||||
// use a server-wide secret and ignore issuers/accounts. In order to bypass this check
|
||||
// you'd have to extract the totp secret from a binary generated by the server.
|
||||
func TOTPServerSecret() (string, error) {
|
||||
secret, err := db.GetKeyValue(TOTPSecretKey)
|
||||
if err == db.ErrRecordNotFound {
|
||||
secret, err = totpGenerateSecret()
|
||||
}
|
||||
return secret, err
|
||||
}
|
||||
|
||||
// ValidateTOTP - Validate a TOTP code
|
||||
func ValidateTOTP(code string) (bool, error) {
|
||||
secret, err := TOTPServerSecret()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
valid, err := totp.ValidateCustom(code, secret, time.Now().UTC(), TOTPOptions())
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return valid, nil
|
||||
}
|
||||
|
||||
func totpGenerateSecret() (string, error) {
|
||||
otpSecret, err := totp.Generate(totp.GenerateOpts{
|
||||
Issuer: "foo",
|
||||
AccountName: "bar",
|
||||
Digits: TOTPDigits,
|
||||
Algorithm: otp.AlgorithmSHA256,
|
||||
Period: TOTPPeriod,
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
secret := otpSecret.Secret()
|
||||
err = db.SetKeyValue(TOTPSecretKey, secret)
|
||||
return secret, err
|
||||
}
|
||||
|
||||
// minisignPrivateKey - This is here so we can marshal to/from JSON
|
||||
type minisignPrivateKey struct {
|
||||
ID uint64 `json:"id"`
|
||||
|
||||
@@ -34,8 +34,8 @@ var (
|
||||
sample1 = randomData()
|
||||
sample2 = randomData()
|
||||
|
||||
serverECCKeyPair *ECCKeyPair
|
||||
implantECCKeyPair *ECCKeyPair
|
||||
serverAgeKeyPair *AgeKeyPair
|
||||
implantPeerAgeKeyPair *AgeKeyPair
|
||||
)
|
||||
|
||||
func randomData() []byte {
|
||||
@@ -51,25 +51,19 @@ func TestMain(m *testing.M) {
|
||||
|
||||
func setup() {
|
||||
var err error
|
||||
serverECCKeyPair, err = RandomECCKeyPair()
|
||||
serverAgeKeyPair, err = RandomAgeKeyPair()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
implantECCKeyPair, err = RandomECCKeyPair()
|
||||
implantPeerAgeKeyPair, err = RandomAgeKeyPair()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
totpSecret, err := TOTPServerSecret()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
implantCrypto.SetSecrets(
|
||||
implantECCKeyPair.PublicBase64(),
|
||||
implantECCKeyPair.PrivateBase64(),
|
||||
MinisignServerSign(implantECCKeyPair.Public[:]),
|
||||
serverECCKeyPair.PublicBase64(),
|
||||
totpSecret,
|
||||
implantPeerAgeKeyPair.Public,
|
||||
implantPeerAgeKeyPair.Private,
|
||||
MinisignServerSign([]byte(implantPeerAgeKeyPair.Public)),
|
||||
serverAgeKeyPair.Public,
|
||||
MinisignServerPublicKey(),
|
||||
)
|
||||
}
|
||||
@@ -171,29 +165,6 @@ func TestCipherContext(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestECCEncryptDecrypt(t *testing.T) {
|
||||
sample := randomData()
|
||||
sender, err := RandomECCKeyPair()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
receiver, err := RandomECCKeyPair()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ciphertext, err := ECCEncrypt(receiver.Public, sender.Private, sample)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
plaintext, err := ECCDecrypt(sender.Public, receiver.Private, ciphertext)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !bytes.Equal(plaintext, sample) {
|
||||
t.Fatalf("Sample does not match decrypted data")
|
||||
}
|
||||
}
|
||||
|
||||
// TestEncryptDecrypt - Test AEAD functions
|
||||
func TestImplantEncryptDecrypt(t *testing.T) {
|
||||
key := RandomKey()
|
||||
@@ -223,42 +194,6 @@ func TestImplantEncryptDecrypt(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestImplantECCEncryptDecrypt(t *testing.T) {
|
||||
sample := randomData()
|
||||
ciphertext, err := implantCrypto.ECCEncryptToServer(sample)
|
||||
if err != nil {
|
||||
t.Fatalf("encrypt to server failed: %s", err)
|
||||
}
|
||||
if len(ciphertext) < 33 {
|
||||
t.Fatalf("ciphertext too short (%d)", len(ciphertext))
|
||||
}
|
||||
|
||||
// Ciphertext has sender public key digest prepended [:32]
|
||||
plaintext, err := ECCDecrypt(implantECCKeyPair.Public, serverECCKeyPair.Private, ciphertext[32:])
|
||||
if err != nil {
|
||||
t.Fatalf("failed to decrypt implant ciphertext: %s", err)
|
||||
}
|
||||
if !bytes.Equal(plaintext, sample) {
|
||||
t.Fatalf("Sample does not match decrypted data")
|
||||
}
|
||||
}
|
||||
|
||||
func TestImplantECCEncryptDecryptTamperData(t *testing.T) {
|
||||
sample := randomData()
|
||||
ciphertext, err := implantCrypto.ECCEncryptToServer(sample)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(ciphertext) < 34 {
|
||||
t.Fatal("ciphertext too short")
|
||||
}
|
||||
ciphertext[33]++ // Change a byte in the ciphertext
|
||||
_, err = ECCDecrypt(implantECCKeyPair.Public, serverECCKeyPair.Private, ciphertext[32:])
|
||||
if err == nil {
|
||||
t.Fatal("ecc decrypted tampered data without error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestServerMinisign(t *testing.T) {
|
||||
message := randomData()
|
||||
privateKey := MinisignServerPrivateKey()
|
||||
|
||||
+14
-16
@@ -279,7 +279,7 @@ func GetSliversDir() string {
|
||||
// -----------------------
|
||||
|
||||
// SliverShellcode - Generates a sliver shellcode using Donut
|
||||
func SliverShellcode(name string, otpSecret string, config *models.ImplantConfig, save bool) (string, error) {
|
||||
func SliverShellcode(name string, config *models.ImplantConfig, save bool) (string, error) {
|
||||
if config.GOOS != "windows" {
|
||||
return "", fmt.Errorf("shellcode format is currently only supported on Windows")
|
||||
}
|
||||
@@ -299,7 +299,7 @@ func SliverShellcode(name string, otpSecret string, config *models.ImplantConfig
|
||||
Obfuscation: config.ObfuscateSymbols,
|
||||
GOGARBLE: goGarble(config),
|
||||
}
|
||||
pkgPath, err := renderSliverGoCode(name, otpSecret, config, goConfig)
|
||||
pkgPath, err := renderSliverGoCode(name, config, goConfig)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@@ -344,7 +344,7 @@ func SliverShellcode(name string, otpSecret string, config *models.ImplantConfig
|
||||
}
|
||||
|
||||
// SliverSharedLibrary - Generates a sliver shared library (DLL/dylib/so) binary
|
||||
func SliverSharedLibrary(name string, otpSecret string, config *models.ImplantConfig, save bool) (string, error) {
|
||||
func SliverSharedLibrary(name string, config *models.ImplantConfig, save bool) (string, error) {
|
||||
// Compile go code
|
||||
var cc string
|
||||
var cxx string
|
||||
@@ -376,7 +376,7 @@ func SliverSharedLibrary(name string, otpSecret string, config *models.ImplantCo
|
||||
Obfuscation: config.ObfuscateSymbols,
|
||||
GOGARBLE: goGarble(config),
|
||||
}
|
||||
pkgPath, err := renderSliverGoCode(name, otpSecret, config, goConfig)
|
||||
pkgPath, err := renderSliverGoCode(name, config, goConfig)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@@ -423,7 +423,7 @@ func SliverSharedLibrary(name string, otpSecret string, config *models.ImplantCo
|
||||
}
|
||||
|
||||
// SliverExecutable - Generates a sliver executable binary
|
||||
func SliverExecutable(name string, otpSecret string, config *models.ImplantConfig, save bool) (string, error) {
|
||||
func SliverExecutable(name string, config *models.ImplantConfig, save bool) (string, error) {
|
||||
// Compile go code
|
||||
appDir := assets.GetRootAppDir()
|
||||
cgo := "0"
|
||||
@@ -446,7 +446,7 @@ func SliverExecutable(name string, otpSecret string, config *models.ImplantConfi
|
||||
GOGARBLE: goGarble(config),
|
||||
}
|
||||
|
||||
pkgPath, err := renderSliverGoCode(name, otpSecret, config, goConfig)
|
||||
pkgPath, err := renderSliverGoCode(name, config, goConfig)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@@ -484,7 +484,7 @@ func SliverExecutable(name string, otpSecret string, config *models.ImplantConfi
|
||||
}
|
||||
|
||||
// This function is a little too long, we should probably refactor it as some point
|
||||
func renderSliverGoCode(name string, otpSecret string, config *models.ImplantConfig, goConfig *gogo.GoConfig) (string, error) {
|
||||
func renderSliverGoCode(name string, config *models.ImplantConfig, goConfig *gogo.GoConfig) (string, error) {
|
||||
target := fmt.Sprintf("%s/%s", config.GOOS, config.GOARCH)
|
||||
if _, ok := gogo.ValidCompilerTargets(*goConfig)[target]; !ok {
|
||||
return "", fmt.Errorf("invalid compiler target: %s", target)
|
||||
@@ -584,12 +584,10 @@ func renderSliverGoCode(name string, otpSecret string, config *models.ImplantCon
|
||||
err = sliverCode.Execute(buf, struct {
|
||||
Name string
|
||||
Config *models.ImplantConfig
|
||||
OTPSecret string
|
||||
HTTPC2ImplantConfig *configs.HTTPC2ImplantConfig
|
||||
}{
|
||||
name,
|
||||
config,
|
||||
otpSecret,
|
||||
configs.GetHTTPC2Config().RandomImplantConfig(),
|
||||
})
|
||||
if err != nil {
|
||||
@@ -773,17 +771,17 @@ func GenerateConfig(name string, config *models.ImplantConfig, save bool) error
|
||||
}
|
||||
|
||||
// ECC keys
|
||||
implantKeyPair, err := cryptography.RandomECCKeyPair()
|
||||
implantKeyPair, err := cryptography.RandomAgeKeyPair()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
serverKeyPair := cryptography.ECCServerKeyPair()
|
||||
digest := sha256.Sum256((*implantKeyPair.Public)[:])
|
||||
config.ECCPublicKey = implantKeyPair.PublicBase64()
|
||||
serverKeyPair := cryptography.AgeServerKeyPair()
|
||||
digest := sha256.Sum256([]byte(implantKeyPair.Public))
|
||||
config.ECCPublicKey = implantKeyPair.Public
|
||||
config.ECCPublicKeyDigest = hex.EncodeToString(digest[:])
|
||||
config.ECCPrivateKey = implantKeyPair.PrivateBase64()
|
||||
config.ECCPublicKeySignature = cryptography.MinisignServerSign(implantKeyPair.Public[:])
|
||||
config.ECCServerPublicKey = serverKeyPair.PublicBase64()
|
||||
config.ECCPrivateKey = implantKeyPair.Private
|
||||
config.ECCPublicKeySignature = cryptography.MinisignServerSign([]byte(implantKeyPair.Public))
|
||||
config.ECCServerPublicKey = serverKeyPair.Public
|
||||
config.MinisignServerPublicKey = cryptography.MinisignServerPublicKey()
|
||||
|
||||
// MTLS keys
|
||||
|
||||
@@ -28,10 +28,6 @@ import (
|
||||
"github.com/bishopfox/sliver/server/db/models"
|
||||
)
|
||||
|
||||
const (
|
||||
otpTestSecret = "12345678901234567890"
|
||||
)
|
||||
|
||||
var (
|
||||
nonce = 0
|
||||
)
|
||||
@@ -191,7 +187,7 @@ func trafficEncodersExecutable(t *testing.T, goos string, goarch string) {
|
||||
TrafficEncodersEnabled: true,
|
||||
}
|
||||
nonce++
|
||||
_, err := SliverExecutable(fmt.Sprintf("trafficEncodersDebug_test%d", nonce), otpTestSecret, debugConfig, true)
|
||||
_, err := SliverExecutable(fmt.Sprintf("trafficEncodersDebug_test%d", nonce), debugConfig, true)
|
||||
if err != nil {
|
||||
t.Fatalf("%v", err)
|
||||
}
|
||||
@@ -209,7 +205,7 @@ func trafficEncodersExecutable(t *testing.T, goos string, goarch string) {
|
||||
TrafficEncodersEnabled: true,
|
||||
}
|
||||
nonce++
|
||||
_, err = SliverExecutable(fmt.Sprintf("trafficEncodersProd_test%d", nonce), otpTestSecret, prodConfig, true)
|
||||
_, err = SliverExecutable(fmt.Sprintf("trafficEncodersProd_test%d", nonce), prodConfig, true)
|
||||
if err != nil {
|
||||
t.Fatalf("%v", err)
|
||||
}
|
||||
@@ -229,7 +225,7 @@ func mtlsExe(t *testing.T, goos string, goarch string, beacon bool, debug bool)
|
||||
IsBeacon: beacon,
|
||||
}
|
||||
nonce++
|
||||
_, err := SliverExecutable(fmt.Sprintf("mtls_test%d", nonce), otpTestSecret, config, true)
|
||||
_, err := SliverExecutable(fmt.Sprintf("mtls_test%d", nonce), config, true)
|
||||
if err != nil {
|
||||
t.Fatalf("%v", err)
|
||||
}
|
||||
@@ -249,7 +245,7 @@ func dnsExe(t *testing.T, goos string, goarch string, beacon bool, debug bool) {
|
||||
IsBeacon: beacon,
|
||||
}
|
||||
nonce++
|
||||
_, err := SliverExecutable(fmt.Sprintf("dns_test%d", nonce), otpTestSecret, config, true)
|
||||
_, err := SliverExecutable(fmt.Sprintf("dns_test%d", nonce), config, true)
|
||||
if err != nil {
|
||||
t.Fatalf("%v", err)
|
||||
}
|
||||
@@ -273,7 +269,7 @@ func httpExe(t *testing.T, goos string, goarch string, beacon bool, debug bool)
|
||||
IsBeacon: beacon,
|
||||
}
|
||||
nonce++
|
||||
_, err := SliverExecutable(fmt.Sprintf("http_test%d", nonce), otpTestSecret, config, true)
|
||||
_, err := SliverExecutable(fmt.Sprintf("http_test%d", nonce), config, true)
|
||||
if err != nil {
|
||||
t.Fatalf("%v", err)
|
||||
}
|
||||
@@ -301,7 +297,7 @@ func multiExe(t *testing.T, goos string, goarch string, beacon bool, debug bool)
|
||||
IsBeacon: beacon,
|
||||
}
|
||||
nonce++
|
||||
_, err := SliverExecutable(fmt.Sprintf("multi_test%d", nonce), otpTestSecret, config, true)
|
||||
_, err := SliverExecutable(fmt.Sprintf("multi_test%d", nonce), config, true)
|
||||
if err != nil {
|
||||
t.Fatalf("%v", err)
|
||||
}
|
||||
@@ -328,7 +324,7 @@ func multiWindowsService(t *testing.T, goos string, goarch string, beacon bool,
|
||||
IsBeacon: beacon,
|
||||
}
|
||||
nonce++
|
||||
_, err := SliverExecutable(fmt.Sprintf("service_test%d", nonce), otpTestSecret, config, true)
|
||||
_, err := SliverExecutable(fmt.Sprintf("service_test%d", nonce), config, true)
|
||||
if err != nil {
|
||||
t.Fatalf("%v", err)
|
||||
}
|
||||
@@ -352,7 +348,7 @@ func tcpPivotExe(t *testing.T, goos string, goarch string, debug bool) {
|
||||
ObfuscateSymbols: false,
|
||||
}
|
||||
nonce++
|
||||
_, err := SliverExecutable(fmt.Sprintf("tcpPivot_test%d", nonce), otpTestSecret, config, true)
|
||||
_, err := SliverExecutable(fmt.Sprintf("tcpPivot_test%d", nonce), config, true)
|
||||
if err != nil {
|
||||
t.Fatalf("%v", err)
|
||||
}
|
||||
@@ -375,7 +371,7 @@ func namedPipeExe(t *testing.T, goos string, goarch string, debug bool) {
|
||||
ObfuscateSymbols: false,
|
||||
}
|
||||
nonce++
|
||||
_, err := SliverExecutable(fmt.Sprintf("namedpipe_test%d", nonce), otpTestSecret, config, true)
|
||||
_, err := SliverExecutable(fmt.Sprintf("namedpipe_test%d", nonce), config, true)
|
||||
if err != nil {
|
||||
t.Fatalf("%v", err)
|
||||
}
|
||||
@@ -405,7 +401,7 @@ func wireguardExe(t *testing.T, goos string, goarch string, beacon bool, debug b
|
||||
}
|
||||
nonce++
|
||||
certs.SetupWGKeys()
|
||||
_, err := SliverExecutable(fmt.Sprintf("wireguard_test%d", nonce), otpTestSecret, config, true)
|
||||
_, err := SliverExecutable(fmt.Sprintf("wireguard_test%d", nonce), config, true)
|
||||
if err != nil {
|
||||
t.Fatalf("%v", err)
|
||||
}
|
||||
@@ -437,7 +433,7 @@ func multiLibrary(t *testing.T, goos string, goarch string, debug bool) {
|
||||
WGTcpCommsPort: 5678,
|
||||
}
|
||||
nonce++
|
||||
_, err := SliverSharedLibrary(fmt.Sprintf("multilibrary_test%d", nonce), otpTestSecret, config, true)
|
||||
_, err := SliverSharedLibrary(fmt.Sprintf("multilibrary_test%d", nonce), config, true)
|
||||
if err != nil {
|
||||
t.Fatalf("%v", err)
|
||||
}
|
||||
@@ -463,7 +459,7 @@ func symbolObfuscation(t *testing.T, goos string, goarch string) {
|
||||
ObfuscateSymbols: true,
|
||||
}
|
||||
nonce++
|
||||
_, err := SliverExecutable(fmt.Sprintf("symbol_test%d", nonce), otpTestSecret, config, true)
|
||||
_, err := SliverExecutable(fmt.Sprintf("symbol_test%d", nonce), config, true)
|
||||
if err != nil {
|
||||
t.Fatalf("%v", err)
|
||||
}
|
||||
|
||||
@@ -20,7 +20,6 @@ package generate
|
||||
|
||||
import (
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
"github.com/bishopfox/sliver/server/cryptography"
|
||||
"github.com/bishopfox/sliver/server/db/models"
|
||||
)
|
||||
|
||||
@@ -30,12 +29,7 @@ func SliverExternal(name string, config *models.ImplantConfig) (*clientpb.Extern
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
otpSecret, err := cryptography.TOTPServerSecret()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &clientpb.ExternalImplantConfig{
|
||||
Config: config.ToProtobuf(),
|
||||
OTPSecret: otpSecret,
|
||||
Config: config.ToProtobuf(),
|
||||
}, nil
|
||||
}
|
||||
@@ -217,8 +217,8 @@ func serverKeyExchange(implantConn *core.ImplantConnection, peerEnvelope *sliver
|
||||
}
|
||||
var senderPublicKey [32]byte
|
||||
copy(senderPublicKey[:], publicKey)
|
||||
serverKeyPair := cryptography.ECCServerKeyPair()
|
||||
rawSessionKey, err := cryptography.ECCDecrypt(&senderPublicKey, serverKeyPair.Private, serverKeyEx.SessionKey[32:])
|
||||
serverKeyPair := cryptography.AgeServerKeyPair()
|
||||
rawSessionKey, err := cryptography.AgeDecrypt(serverKeyPair.Private, serverKeyEx.SessionKey[32:])
|
||||
if err != nil {
|
||||
pivotLog.Warn("Failed to decrypt session key from origin")
|
||||
return nil
|
||||
|
||||
@@ -29,7 +29,6 @@ import (
|
||||
"github.com/bishopfox/sliver/protobuf/sliverpb"
|
||||
"github.com/bishopfox/sliver/server/codenames"
|
||||
"github.com/bishopfox/sliver/server/core"
|
||||
"github.com/bishopfox/sliver/server/cryptography"
|
||||
"github.com/bishopfox/sliver/server/generate"
|
||||
"github.com/bishopfox/sliver/util/encoders"
|
||||
"google.golang.org/grpc/codes"
|
||||
@@ -86,12 +85,11 @@ func (rpc *Server) Backdoor(ctx context.Context, req *clientpb.BackdoorReq) (*cl
|
||||
}
|
||||
|
||||
name, config := generate.ImplantConfigFromProtobuf(p.Config)
|
||||
otpSecret, _ := cryptography.TOTPServerSecret()
|
||||
err = generate.GenerateConfig(name, config, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
fPath, err := generate.SliverShellcode(name, otpSecret, config, true)
|
||||
fPath, err := generate.SliverShellcode(name, config, true)
|
||||
if err != nil {
|
||||
return nil, status.Error(codes.Internal, err.Error())
|
||||
}
|
||||
|
||||
@@ -35,7 +35,6 @@ import (
|
||||
"github.com/bishopfox/sliver/server/assets"
|
||||
"github.com/bishopfox/sliver/server/codenames"
|
||||
"github.com/bishopfox/sliver/server/core"
|
||||
"github.com/bishopfox/sliver/server/cryptography"
|
||||
"github.com/bishopfox/sliver/server/db"
|
||||
"github.com/bishopfox/sliver/server/encoders"
|
||||
"github.com/bishopfox/sliver/server/generate"
|
||||
@@ -67,7 +66,6 @@ func (rpc *Server) Generate(ctx context.Context, req *clientpb.GenerateReq) (*cl
|
||||
config.TemplateName = generate.SliverTemplateName
|
||||
}
|
||||
|
||||
otpSecret, _ := cryptography.TOTPServerSecret()
|
||||
err = generate.GenerateConfig(name, config, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -79,11 +77,11 @@ func (rpc *Server) Generate(ctx context.Context, req *clientpb.GenerateReq) (*cl
|
||||
case clientpb.OutputFormat_SERVICE:
|
||||
fallthrough
|
||||
case clientpb.OutputFormat_EXECUTABLE:
|
||||
fPath, err = generate.SliverExecutable(name, otpSecret, config, true)
|
||||
fPath, err = generate.SliverExecutable(name, config, true)
|
||||
case clientpb.OutputFormat_SHARED_LIB:
|
||||
fPath, err = generate.SliverSharedLibrary(name, otpSecret, config, true)
|
||||
fPath, err = generate.SliverSharedLibrary(name, config, true)
|
||||
case clientpb.OutputFormat_SHELLCODE:
|
||||
fPath, err = generate.SliverShellcode(name, otpSecret, config, true)
|
||||
fPath, err = generate.SliverShellcode(name, config, true)
|
||||
default:
|
||||
return nil, fmt.Errorf("invalid output format: %s", req.Config.Format)
|
||||
}
|
||||
@@ -359,14 +357,8 @@ func (rpc *Server) GenerateExternalGetImplantConfig(ctx context.Context, req *cl
|
||||
return nil, status.Error(codes.InvalidArgument, "implant config already has a build")
|
||||
}
|
||||
|
||||
otpSecret, err := cryptography.TOTPServerSecret()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &clientpb.ExternalImplantConfig{
|
||||
Config: implantConfig.ToProtobuf(),
|
||||
OTPSecret: otpSecret,
|
||||
Config: implantConfig.ToProtobuf(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -36,7 +36,6 @@ import (
|
||||
"github.com/bishopfox/sliver/protobuf/sliverpb"
|
||||
"github.com/bishopfox/sliver/server/codenames"
|
||||
"github.com/bishopfox/sliver/server/core"
|
||||
"github.com/bishopfox/sliver/server/cryptography"
|
||||
"github.com/bishopfox/sliver/server/generate"
|
||||
"github.com/bishopfox/sliver/util/encoders"
|
||||
)
|
||||
@@ -113,12 +112,11 @@ func (rpc *Server) HijackDLL(ctx context.Context, req *clientpb.DllHijackReq) (*
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
otpSecret, _ := cryptography.TOTPServerSecret()
|
||||
err = generate.GenerateConfig(name, config, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
fPath, err := generate.SliverSharedLibrary(name, otpSecret, config, true)
|
||||
fPath, err := generate.SliverSharedLibrary(name, config, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -28,7 +28,6 @@ import (
|
||||
"github.com/bishopfox/sliver/protobuf/sliverpb"
|
||||
"github.com/bishopfox/sliver/server/codenames"
|
||||
"github.com/bishopfox/sliver/server/core"
|
||||
"github.com/bishopfox/sliver/server/cryptography"
|
||||
"github.com/bishopfox/sliver/server/generate"
|
||||
|
||||
"google.golang.org/protobuf/proto"
|
||||
@@ -94,12 +93,11 @@ func (rpc *Server) GetSystem(ctx context.Context, req *clientpb.GetSystemReq) (*
|
||||
}
|
||||
config.Format = clientpb.OutputFormat_SHELLCODE
|
||||
config.ObfuscateSymbols = false
|
||||
otpSecret, _ := cryptography.TOTPServerSecret()
|
||||
err = generate.GenerateConfig(name, config, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
shellcodePath, err := generate.SliverShellcode(name, otpSecret, config, true)
|
||||
shellcodePath, err := generate.SliverShellcode(name, config, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -33,7 +33,6 @@ import (
|
||||
"github.com/bishopfox/sliver/protobuf/sliverpb"
|
||||
"github.com/bishopfox/sliver/server/codenames"
|
||||
"github.com/bishopfox/sliver/server/core"
|
||||
"github.com/bishopfox/sliver/server/cryptography"
|
||||
"github.com/bishopfox/sliver/server/db"
|
||||
"github.com/bishopfox/sliver/server/db/models"
|
||||
"github.com/bishopfox/sliver/server/generate"
|
||||
@@ -78,12 +77,11 @@ func (rpc *Server) Migrate(ctx context.Context, req *clientpb.MigrateReq) (*sliv
|
||||
}
|
||||
config.Format = clientpb.OutputFormat_SHELLCODE
|
||||
config.ObfuscateSymbols = true
|
||||
otpSecret, _ := cryptography.TOTPServerSecret()
|
||||
err = generate.GenerateConfig(name, config, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
shellcodePath, err := generate.SliverShellcode(name, otpSecret, config, true)
|
||||
shellcodePath, err := generate.SliverShellcode(name, config, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
Generated
+2
@@ -0,0 +1,2 @@
|
||||
*.age binary
|
||||
testdata/testkit/* binary
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
# This is the official list of age authors for copyright purposes.
|
||||
# To be included, send a change adding the individual or company
|
||||
# who owns a contribution's copyright.
|
||||
|
||||
Google LLC
|
||||
Filippo Valsorda
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
Copyright 2019 The age Authors
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above
|
||||
copyright notice, this list of conditions and the following disclaimer
|
||||
in the documentation and/or other materials provided with the
|
||||
distribution.
|
||||
* Neither the name of the age project nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
+251
@@ -0,0 +1,251 @@
|
||||
<p align="center"><img alt="The age logo, an wireframe of St. Peters dome in Rome, with the text: age, file encryption" width="600" src="https://user-images.githubusercontent.com/1225294/132245842-fda4da6a-1cea-4738-a3da-2dc860861c98.png"></p>
|
||||
|
||||
[](https://pkg.go.dev/filippo.io/age)
|
||||
[-man%20page-lightgrey>)](https://filippo.io/age/age.1)
|
||||
[](https://age-encryption.org/v1)
|
||||
|
||||
age is a simple, modern and secure file encryption tool, format, and Go library.
|
||||
|
||||
It features small explicit keys, no config options, and UNIX-style composability.
|
||||
|
||||
```
|
||||
$ age-keygen -o key.txt
|
||||
Public key: age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p
|
||||
$ tar cvz ~/data | age -r age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p > data.tar.gz.age
|
||||
$ age --decrypt -i key.txt data.tar.gz.age > data.tar.gz
|
||||
```
|
||||
|
||||
📜 The format specification is at [age-encryption.org/v1](https://age-encryption.org/v1). age was designed by [@Benjojo12](https://twitter.com/Benjojo12) and [@FiloSottile](https://twitter.com/FiloSottile).
|
||||
|
||||
📬 Follow the maintenance of this project by subscribing to [Maintainer Dispatches](https://filippo.io/newsletter)!
|
||||
|
||||
🦀 An alternative interoperable Rust implementation is available at [github.com/str4d/rage](https://github.com/str4d/rage).
|
||||
|
||||
🔑 Hardware PIV tokens such as YubiKeys are supported through the [age-plugin-yubikey](https://github.com/str4d/age-plugin-yubikey) plugin.
|
||||
|
||||
💬 The author pronounces it `[aɡe̞]`, like the Italian [“aghe”](https://translate.google.com/?sl=it&text=aghe).
|
||||
|
||||
## Installation
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<td>Homebrew (macOS or Linux)</td>
|
||||
<td>
|
||||
<code>brew install age</code>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>MacPorts</td>
|
||||
<td>
|
||||
<code>port install age</code>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Alpine Linux v3.15+</td>
|
||||
<td>
|
||||
<code>apk add age</code>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Arch Linux</td>
|
||||
<td>
|
||||
<code>pacman -S age</code>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Debian 11+ (Bullseye)</td>
|
||||
<td>
|
||||
<code>apt install age/bullseye-backports</code>
|
||||
(<a href="https://backports.debian.org/Instructions/#index2h2">enable backports</a> for age v1.0.0+)
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Fedora 33+</td>
|
||||
<td>
|
||||
<code>dnf install age</code>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Gentoo Linux</td>
|
||||
<td>
|
||||
<code>emerge app-crypt/age</code>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>NixOS / Nix</td>
|
||||
<td>
|
||||
<code>nix-env -i age</code>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>openSUSE Tumbleweed</td>
|
||||
<td>
|
||||
<code>zypper install age</code>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Ubuntu 22.04+</td>
|
||||
<td>
|
||||
<code>apt install age</code>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Void Linux</td>
|
||||
<td>
|
||||
<code>xbps-install age</code>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>FreeBSD</td>
|
||||
<td>
|
||||
<code>pkg install age</code> (security/age)
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>OpenBSD 6.7+</td>
|
||||
<td>
|
||||
<code>pkg_add age</code> (security/age)
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Chocolatey (Windows)</td>
|
||||
<td>
|
||||
<code>choco install age.portable</code>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Scoop (Windows)</td>
|
||||
<td>
|
||||
<code>scoop bucket add extras; scoop install age</code>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
On Windows, Linux, macOS, and FreeBSD you can use the pre-built binaries.
|
||||
|
||||
```
|
||||
https://dl.filippo.io/age/latest?for=linux/amd64
|
||||
https://dl.filippo.io/age/v1.0.0-rc.1?for=darwin/arm64
|
||||
...
|
||||
```
|
||||
|
||||
If your system has [a supported version of Go](https://go.dev/dl/), you can build from source.
|
||||
|
||||
```
|
||||
go install filippo.io/age/cmd/...@latest
|
||||
```
|
||||
|
||||
Help from new packagers is very welcome.
|
||||
|
||||
## Usage
|
||||
|
||||
For the full documentation, read [the age(1) man page](https://filippo.io/age/age.1).
|
||||
|
||||
```
|
||||
Usage:
|
||||
age [--encrypt] (-r RECIPIENT | -R PATH)... [--armor] [-o OUTPUT] [INPUT]
|
||||
age [--encrypt] --passphrase [--armor] [-o OUTPUT] [INPUT]
|
||||
age --decrypt [-i PATH]... [-o OUTPUT] [INPUT]
|
||||
|
||||
Options:
|
||||
-e, --encrypt Encrypt the input to the output. Default if omitted.
|
||||
-d, --decrypt Decrypt the input to the output.
|
||||
-o, --output OUTPUT Write the result to the file at path OUTPUT.
|
||||
-a, --armor Encrypt to a PEM encoded format.
|
||||
-p, --passphrase Encrypt with a passphrase.
|
||||
-r, --recipient RECIPIENT Encrypt to the specified RECIPIENT. Can be repeated.
|
||||
-R, --recipients-file PATH Encrypt to recipients listed at PATH. Can be repeated.
|
||||
-i, --identity PATH Use the identity file at PATH. Can be repeated.
|
||||
|
||||
INPUT defaults to standard input, and OUTPUT defaults to standard output.
|
||||
If OUTPUT exists, it will be overwritten.
|
||||
|
||||
RECIPIENT can be an age public key generated by age-keygen ("age1...")
|
||||
or an SSH public key ("ssh-ed25519 AAAA...", "ssh-rsa AAAA...").
|
||||
|
||||
Recipient files contain one or more recipients, one per line. Empty lines
|
||||
and lines starting with "#" are ignored as comments. "-" may be used to
|
||||
read recipients from standard input.
|
||||
|
||||
Identity files contain one or more secret keys ("AGE-SECRET-KEY-1..."),
|
||||
one per line, or an SSH key. Empty lines and lines starting with "#" are
|
||||
ignored as comments. Passphrase encrypted age files can be used as
|
||||
identity files. Multiple key files can be provided, and any unused ones
|
||||
will be ignored. "-" may be used to read identities from standard input.
|
||||
|
||||
When --encrypt is specified explicitly, -i can also be used to encrypt to an
|
||||
identity file symmetrically, instead or in addition to normal recipients.
|
||||
```
|
||||
|
||||
### Multiple recipients
|
||||
|
||||
Files can be encrypted to multiple recipients by repeating `-r/--recipient`. Every recipient will be able to decrypt the file.
|
||||
|
||||
```
|
||||
$ age -o example.jpg.age -r age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p \
|
||||
-r age1lggyhqrw2nlhcxprm67z43rta597azn8gknawjehu9d9dl0jq3yqqvfafg example.jpg
|
||||
```
|
||||
|
||||
#### Recipient files
|
||||
|
||||
Multiple recipients can also be listed one per line in one or more files passed with the `-R/--recipients-file` flag.
|
||||
|
||||
```
|
||||
$ cat recipients.txt
|
||||
# Alice
|
||||
age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p
|
||||
# Bob
|
||||
age1lggyhqrw2nlhcxprm67z43rta597azn8gknawjehu9d9dl0jq3yqqvfafg
|
||||
$ age -R recipients.txt example.jpg > example.jpg.age
|
||||
```
|
||||
|
||||
If the argument to `-R` (or `-i`) is `-`, the file is read from standard input.
|
||||
|
||||
### Passphrases
|
||||
|
||||
Files can be encrypted with a passphrase by using `-p/--passphrase`. By default age will automatically generate a secure passphrase. Passphrase protected files are automatically detected at decrypt time.
|
||||
|
||||
```
|
||||
$ age -p secrets.txt > secrets.txt.age
|
||||
Enter passphrase (leave empty to autogenerate a secure one):
|
||||
Using the autogenerated passphrase "release-response-step-brand-wrap-ankle-pair-unusual-sword-train".
|
||||
$ age -d secrets.txt.age > secrets.txt
|
||||
Enter passphrase:
|
||||
```
|
||||
|
||||
### Passphrase-protected key files
|
||||
|
||||
If an identity file passed to `-i` is a passphrase encrypted age file, it will be automatically decrypted.
|
||||
|
||||
```
|
||||
$ age-keygen | age -p > key.age
|
||||
Public key: age1yhm4gctwfmrpz87tdslm550wrx6m79y9f2hdzt0lndjnehwj0ukqrjpyx5
|
||||
Enter passphrase (leave empty to autogenerate a secure one):
|
||||
Using the autogenerated passphrase "hip-roast-boring-snake-mention-east-wasp-honey-input-actress".
|
||||
$ age -r age1yhm4gctwfmrpz87tdslm550wrx6m79y9f2hdzt0lndjnehwj0ukqrjpyx5 secrets.txt > secrets.txt.age
|
||||
$ age -d -i key.age secrets.txt.age > secrets.txt
|
||||
Enter passphrase for identity file "key.age":
|
||||
```
|
||||
|
||||
Passphrase-protected identity files are not necessary for most use cases, where access to the encrypted identity file implies access to the whole system. However, they can be useful if the identity file is stored remotely.
|
||||
|
||||
### SSH keys
|
||||
|
||||
As a convenience feature, age also supports encrypting to `ssh-rsa` and `ssh-ed25519` SSH public keys, and decrypting with the respective private key file. (`ssh-agent` is not supported.)
|
||||
|
||||
```
|
||||
$ age -R ~/.ssh/id_ed25519.pub example.jpg > example.jpg.age
|
||||
$ age -d -i ~/.ssh/id_ed25519 example.jpg.age > example.jpg
|
||||
```
|
||||
|
||||
Note that SSH key support employs more complex cryptography, and embeds a public key tag in the encrypted file, making it possible to track files that are encrypted to a specific public key.
|
||||
|
||||
#### Encrypting to a GitHub user
|
||||
|
||||
Combining SSH key support and `-R`, you can easily encrypt a file to the SSH keys listed on a GitHub profile.
|
||||
|
||||
```
|
||||
$ curl https://github.com/benjojo.keys | age -R - example.jpg > example.jpg.age
|
||||
```
|
||||
|
||||
Keep in mind that people might not protect SSH keys long-term, since they are revokable when used only for authentication, and that SSH keys held on YubiKeys can't be used to decrypt files.
|
||||
+238
@@ -0,0 +1,238 @@
|
||||
// Copyright 2019 The age Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package age implements file encryption according to the age-encryption.org/v1
|
||||
// specification.
|
||||
//
|
||||
// For most use cases, use the Encrypt and Decrypt functions with
|
||||
// X25519Recipient and X25519Identity. If passphrase encryption is required, use
|
||||
// ScryptRecipient and ScryptIdentity. For compatibility with existing SSH keys
|
||||
// use the filippo.io/age/agessh package.
|
||||
//
|
||||
// age encrypted files are binary and not malleable. For encoding them as text,
|
||||
// use the filippo.io/age/armor package.
|
||||
//
|
||||
// Key management
|
||||
//
|
||||
// age does not have a global keyring. Instead, since age keys are small,
|
||||
// textual, and cheap, you are encouraged to generate dedicated keys for each
|
||||
// task and application.
|
||||
//
|
||||
// Recipient public keys can be passed around as command line flags and in
|
||||
// config files, while secret keys should be stored in dedicated files, through
|
||||
// secret management systems, or as environment variables.
|
||||
//
|
||||
// There is no default path for age keys. Instead, they should be stored at
|
||||
// application-specific paths. The CLI supports files where private keys are
|
||||
// listed one per line, ignoring empty lines and lines starting with "#". These
|
||||
// files can be parsed with ParseIdentities.
|
||||
//
|
||||
// When integrating age into a new system, it's recommended that you only
|
||||
// support X25519 keys, and not SSH keys. The latter are supported for manual
|
||||
// encryption operations. If you need to tie into existing key management
|
||||
// infrastructure, you might want to consider implementing your own Recipient
|
||||
// and Identity.
|
||||
//
|
||||
// Backwards compatibility
|
||||
//
|
||||
// Files encrypted with a stable version (not alpha, beta, or release candidate)
|
||||
// of age, or with any v1.0.0 beta or release candidate, will decrypt with any
|
||||
// later versions of the v1 API. This might change in v2, in which case v1 will
|
||||
// be maintained with security fixes for compatibility with older files.
|
||||
//
|
||||
// If decrypting an older file poses a security risk, doing so might require an
|
||||
// explicit opt-in in the API.
|
||||
package age
|
||||
|
||||
import (
|
||||
"crypto/hmac"
|
||||
"crypto/rand"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"filippo.io/age/internal/format"
|
||||
"filippo.io/age/internal/stream"
|
||||
)
|
||||
|
||||
// An Identity is passed to Decrypt to unwrap an opaque file key from a
|
||||
// recipient stanza. It can be for example a secret key like X25519Identity, a
|
||||
// plugin, or a custom implementation.
|
||||
//
|
||||
// Unwrap must return an error wrapping ErrIncorrectIdentity if none of the
|
||||
// recipient stanzas match the identity, any other error will be considered
|
||||
// fatal.
|
||||
//
|
||||
// Most age API users won't need to interact with this directly, and should
|
||||
// instead pass Recipient implementations to Encrypt and Identity
|
||||
// implementations to Decrypt.
|
||||
type Identity interface {
|
||||
Unwrap(stanzas []*Stanza) (fileKey []byte, err error)
|
||||
}
|
||||
|
||||
var ErrIncorrectIdentity = errors.New("incorrect identity for recipient block")
|
||||
|
||||
// A Recipient is passed to Encrypt to wrap an opaque file key to one or more
|
||||
// recipient stanza(s). It can be for example a public key like X25519Recipient,
|
||||
// a plugin, or a custom implementation.
|
||||
//
|
||||
// Most age API users won't need to interact with this directly, and should
|
||||
// instead pass Recipient implementations to Encrypt and Identity
|
||||
// implementations to Decrypt.
|
||||
type Recipient interface {
|
||||
Wrap(fileKey []byte) ([]*Stanza, error)
|
||||
}
|
||||
|
||||
// A Stanza is a section of the age header that encapsulates the file key as
|
||||
// encrypted to a specific recipient.
|
||||
//
|
||||
// Most age API users won't need to interact with this directly, and should
|
||||
// instead pass Recipient implementations to Encrypt and Identity
|
||||
// implementations to Decrypt.
|
||||
type Stanza struct {
|
||||
Type string
|
||||
Args []string
|
||||
Body []byte
|
||||
}
|
||||
|
||||
const fileKeySize = 16
|
||||
const streamNonceSize = 16
|
||||
|
||||
// Encrypt encrypts a file to one or more recipients.
|
||||
//
|
||||
// Writes to the returned WriteCloser are encrypted and written to dst as an age
|
||||
// file. Every recipient will be able to decrypt the file.
|
||||
//
|
||||
// The caller must call Close on the WriteCloser when done for the last chunk to
|
||||
// be encrypted and flushed to dst.
|
||||
func Encrypt(dst io.Writer, recipients ...Recipient) (io.WriteCloser, error) {
|
||||
if len(recipients) == 0 {
|
||||
return nil, errors.New("no recipients specified")
|
||||
}
|
||||
|
||||
// As a best effort, prevent an API user from generating a file that the
|
||||
// ScryptIdentity will refuse to decrypt. This check can't unfortunately be
|
||||
// implemented as part of the Recipient interface, so it lives as a special
|
||||
// case in Encrypt.
|
||||
for _, r := range recipients {
|
||||
if _, ok := r.(*ScryptRecipient); ok && len(recipients) != 1 {
|
||||
return nil, errors.New("an ScryptRecipient must be the only one for the file")
|
||||
}
|
||||
}
|
||||
|
||||
fileKey := make([]byte, fileKeySize)
|
||||
if _, err := rand.Read(fileKey); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
hdr := &format.Header{}
|
||||
for i, r := range recipients {
|
||||
stanzas, err := r.Wrap(fileKey)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to wrap key for recipient #%d: %v", i, err)
|
||||
}
|
||||
for _, s := range stanzas {
|
||||
hdr.Recipients = append(hdr.Recipients, (*format.Stanza)(s))
|
||||
}
|
||||
}
|
||||
if mac, err := headerMAC(fileKey, hdr); err != nil {
|
||||
return nil, fmt.Errorf("failed to compute header MAC: %v", err)
|
||||
} else {
|
||||
hdr.MAC = mac
|
||||
}
|
||||
if err := hdr.Marshal(dst); err != nil {
|
||||
return nil, fmt.Errorf("failed to write header: %v", err)
|
||||
}
|
||||
|
||||
nonce := make([]byte, streamNonceSize)
|
||||
if _, err := rand.Read(nonce); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, err := dst.Write(nonce); err != nil {
|
||||
return nil, fmt.Errorf("failed to write nonce: %v", err)
|
||||
}
|
||||
|
||||
return stream.NewWriter(streamKey(fileKey, nonce), dst)
|
||||
}
|
||||
|
||||
// NoIdentityMatchError is returned by Decrypt when none of the supplied
|
||||
// identities match the encrypted file.
|
||||
type NoIdentityMatchError struct {
|
||||
// Errors is a slice of all the errors returned to Decrypt by the Unwrap
|
||||
// calls it made. They all wrap ErrIncorrectIdentity.
|
||||
Errors []error
|
||||
}
|
||||
|
||||
func (*NoIdentityMatchError) Error() string {
|
||||
return "no identity matched any of the recipients"
|
||||
}
|
||||
|
||||
// Decrypt decrypts a file encrypted to one or more identities.
|
||||
//
|
||||
// It returns a Reader reading the decrypted plaintext of the age file read
|
||||
// from src. All identities will be tried until one successfully decrypts the file.
|
||||
func Decrypt(src io.Reader, identities ...Identity) (io.Reader, error) {
|
||||
if len(identities) == 0 {
|
||||
return nil, errors.New("no identities specified")
|
||||
}
|
||||
|
||||
hdr, payload, err := format.Parse(src)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read header: %w", err)
|
||||
}
|
||||
|
||||
stanzas := make([]*Stanza, 0, len(hdr.Recipients))
|
||||
for _, s := range hdr.Recipients {
|
||||
stanzas = append(stanzas, (*Stanza)(s))
|
||||
}
|
||||
errNoMatch := &NoIdentityMatchError{}
|
||||
var fileKey []byte
|
||||
for _, id := range identities {
|
||||
fileKey, err = id.Unwrap(stanzas)
|
||||
if errors.Is(err, ErrIncorrectIdentity) {
|
||||
errNoMatch.Errors = append(errNoMatch.Errors, err)
|
||||
continue
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
if fileKey == nil {
|
||||
return nil, errNoMatch
|
||||
}
|
||||
|
||||
if mac, err := headerMAC(fileKey, hdr); err != nil {
|
||||
return nil, fmt.Errorf("failed to compute header MAC: %v", err)
|
||||
} else if !hmac.Equal(mac, hdr.MAC) {
|
||||
return nil, errors.New("bad header MAC")
|
||||
}
|
||||
|
||||
nonce := make([]byte, streamNonceSize)
|
||||
if _, err := io.ReadFull(payload, nonce); err != nil {
|
||||
return nil, fmt.Errorf("failed to read nonce: %w", err)
|
||||
}
|
||||
|
||||
return stream.NewReader(streamKey(fileKey, nonce), payload)
|
||||
}
|
||||
|
||||
// multiUnwrap is a helper that implements Identity.Unwrap in terms of a
|
||||
// function that unwraps a single recipient stanza.
|
||||
func multiUnwrap(unwrap func(*Stanza) ([]byte, error), stanzas []*Stanza) ([]byte, error) {
|
||||
for _, s := range stanzas {
|
||||
fileKey, err := unwrap(s)
|
||||
if errors.Is(err, ErrIncorrectIdentity) {
|
||||
// If we ever start returning something interesting wrapping
|
||||
// ErrIncorrectIdentity, we should let it make its way up through
|
||||
// Decrypt into NoIdentityMatchError.Errors.
|
||||
continue
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return fileKey, nil
|
||||
}
|
||||
return nil, ErrIncorrectIdentity
|
||||
}
|
||||
+173
@@ -0,0 +1,173 @@
|
||||
// Copyright (c) 2017 Takatoshi Nakagawa
|
||||
// Copyright (c) 2019 The age Authors
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
|
||||
// Package bech32 is a modified version of the reference implementation of BIP173.
|
||||
package bech32
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var charset = "qpzry9x8gf2tvdw0s3jn54khce6mua7l"
|
||||
|
||||
var generator = []uint32{0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3}
|
||||
|
||||
func polymod(values []byte) uint32 {
|
||||
chk := uint32(1)
|
||||
for _, v := range values {
|
||||
top := chk >> 25
|
||||
chk = (chk & 0x1ffffff) << 5
|
||||
chk = chk ^ uint32(v)
|
||||
for i := 0; i < 5; i++ {
|
||||
bit := top >> i & 1
|
||||
if bit == 1 {
|
||||
chk ^= generator[i]
|
||||
}
|
||||
}
|
||||
}
|
||||
return chk
|
||||
}
|
||||
|
||||
func hrpExpand(hrp string) []byte {
|
||||
h := []byte(strings.ToLower(hrp))
|
||||
var ret []byte
|
||||
for _, c := range h {
|
||||
ret = append(ret, c>>5)
|
||||
}
|
||||
ret = append(ret, 0)
|
||||
for _, c := range h {
|
||||
ret = append(ret, c&31)
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func verifyChecksum(hrp string, data []byte) bool {
|
||||
return polymod(append(hrpExpand(hrp), data...)) == 1
|
||||
}
|
||||
|
||||
func createChecksum(hrp string, data []byte) []byte {
|
||||
values := append(hrpExpand(hrp), data...)
|
||||
values = append(values, []byte{0, 0, 0, 0, 0, 0}...)
|
||||
mod := polymod(values) ^ 1
|
||||
ret := make([]byte, 6)
|
||||
for p := range ret {
|
||||
shift := 5 * (5 - p)
|
||||
ret[p] = byte(mod>>shift) & 31
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func convertBits(data []byte, frombits, tobits byte, pad bool) ([]byte, error) {
|
||||
var ret []byte
|
||||
acc := uint32(0)
|
||||
bits := byte(0)
|
||||
maxv := byte(1<<tobits - 1)
|
||||
for idx, value := range data {
|
||||
if value>>frombits != 0 {
|
||||
return nil, fmt.Errorf("invalid data range: data[%d]=%d (frombits=%d)", idx, value, frombits)
|
||||
}
|
||||
acc = acc<<frombits | uint32(value)
|
||||
bits += frombits
|
||||
for bits >= tobits {
|
||||
bits -= tobits
|
||||
ret = append(ret, byte(acc>>bits)&maxv)
|
||||
}
|
||||
}
|
||||
if pad {
|
||||
if bits > 0 {
|
||||
ret = append(ret, byte(acc<<(tobits-bits))&maxv)
|
||||
}
|
||||
} else if bits >= frombits {
|
||||
return nil, fmt.Errorf("illegal zero padding")
|
||||
} else if byte(acc<<(tobits-bits))&maxv != 0 {
|
||||
return nil, fmt.Errorf("non-zero padding")
|
||||
}
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
// Encode encodes the HRP and a bytes slice to Bech32. If the HRP is uppercase,
|
||||
// the output will be uppercase.
|
||||
func Encode(hrp string, data []byte) (string, error) {
|
||||
values, err := convertBits(data, 8, 5, true)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if len(hrp) < 1 {
|
||||
return "", fmt.Errorf("invalid HRP: %q", hrp)
|
||||
}
|
||||
for p, c := range hrp {
|
||||
if c < 33 || c > 126 {
|
||||
return "", fmt.Errorf("invalid HRP character: hrp[%d]=%d", p, c)
|
||||
}
|
||||
}
|
||||
if strings.ToUpper(hrp) != hrp && strings.ToLower(hrp) != hrp {
|
||||
return "", fmt.Errorf("mixed case HRP: %q", hrp)
|
||||
}
|
||||
lower := strings.ToLower(hrp) == hrp
|
||||
hrp = strings.ToLower(hrp)
|
||||
var ret strings.Builder
|
||||
ret.WriteString(hrp)
|
||||
ret.WriteString("1")
|
||||
for _, p := range values {
|
||||
ret.WriteByte(charset[p])
|
||||
}
|
||||
for _, p := range createChecksum(hrp, values) {
|
||||
ret.WriteByte(charset[p])
|
||||
}
|
||||
if lower {
|
||||
return ret.String(), nil
|
||||
}
|
||||
return strings.ToUpper(ret.String()), nil
|
||||
}
|
||||
|
||||
// Decode decodes a Bech32 string. If the string is uppercase, the HRP will be uppercase.
|
||||
func Decode(s string) (hrp string, data []byte, err error) {
|
||||
if strings.ToLower(s) != s && strings.ToUpper(s) != s {
|
||||
return "", nil, fmt.Errorf("mixed case")
|
||||
}
|
||||
pos := strings.LastIndex(s, "1")
|
||||
if pos < 1 || pos+7 > len(s) {
|
||||
return "", nil, fmt.Errorf("separator '1' at invalid position: pos=%d, len=%d", pos, len(s))
|
||||
}
|
||||
hrp = s[:pos]
|
||||
for p, c := range hrp {
|
||||
if c < 33 || c > 126 {
|
||||
return "", nil, fmt.Errorf("invalid character human-readable part: s[%d]=%d", p, c)
|
||||
}
|
||||
}
|
||||
s = strings.ToLower(s)
|
||||
for p, c := range s[pos+1:] {
|
||||
d := strings.IndexRune(charset, c)
|
||||
if d == -1 {
|
||||
return "", nil, fmt.Errorf("invalid character data part: s[%d]=%v", p, c)
|
||||
}
|
||||
data = append(data, byte(d))
|
||||
}
|
||||
if !verifyChecksum(hrp, data) {
|
||||
return "", nil, fmt.Errorf("invalid checksum")
|
||||
}
|
||||
data, err = convertBits(data[:len(data)-6], 5, 8, false)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
return hrp, data, nil
|
||||
}
|
||||
+311
@@ -0,0 +1,311 @@
|
||||
// Copyright 2019 The age Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package format implements the age file format.
|
||||
package format
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Header struct {
|
||||
Recipients []*Stanza
|
||||
MAC []byte
|
||||
}
|
||||
|
||||
// Stanza is assignable to age.Stanza, and if this package is made public,
|
||||
// age.Stanza can be made a type alias of this type.
|
||||
type Stanza struct {
|
||||
Type string
|
||||
Args []string
|
||||
Body []byte
|
||||
}
|
||||
|
||||
var b64 = base64.RawStdEncoding.Strict()
|
||||
|
||||
func DecodeString(s string) ([]byte, error) {
|
||||
// CR and LF are ignored by DecodeString, but we don't want any malleability.
|
||||
if strings.ContainsAny(s, "\n\r") {
|
||||
return nil, errors.New(`unexpected newline character`)
|
||||
}
|
||||
return b64.DecodeString(s)
|
||||
}
|
||||
|
||||
var EncodeToString = b64.EncodeToString
|
||||
|
||||
const ColumnsPerLine = 64
|
||||
|
||||
const BytesPerLine = ColumnsPerLine / 4 * 3
|
||||
|
||||
// NewWrappedBase64Encoder returns a WrappedBase64Encoder that writes to dst.
|
||||
func NewWrappedBase64Encoder(enc *base64.Encoding, dst io.Writer) *WrappedBase64Encoder {
|
||||
w := &WrappedBase64Encoder{dst: dst}
|
||||
w.enc = base64.NewEncoder(enc, WriterFunc(w.writeWrapped))
|
||||
return w
|
||||
}
|
||||
|
||||
type WriterFunc func(p []byte) (int, error)
|
||||
|
||||
func (f WriterFunc) Write(p []byte) (int, error) { return f(p) }
|
||||
|
||||
// WrappedBase64Encoder is a standard base64 encoder that inserts an LF
|
||||
// character every ColumnsPerLine bytes. It does not insert a newline neither at
|
||||
// the beginning nor at the end of the stream, but it ensures the last line is
|
||||
// shorter than ColumnsPerLine, which means it might be empty.
|
||||
type WrappedBase64Encoder struct {
|
||||
enc io.WriteCloser
|
||||
dst io.Writer
|
||||
written int
|
||||
buf bytes.Buffer
|
||||
}
|
||||
|
||||
func (w *WrappedBase64Encoder) Write(p []byte) (int, error) { return w.enc.Write(p) }
|
||||
|
||||
func (w *WrappedBase64Encoder) Close() error {
|
||||
return w.enc.Close()
|
||||
}
|
||||
|
||||
func (w *WrappedBase64Encoder) writeWrapped(p []byte) (int, error) {
|
||||
if w.buf.Len() != 0 {
|
||||
panic("age: internal error: non-empty WrappedBase64Encoder.buf")
|
||||
}
|
||||
for len(p) > 0 {
|
||||
toWrite := ColumnsPerLine - (w.written % ColumnsPerLine)
|
||||
if toWrite > len(p) {
|
||||
toWrite = len(p)
|
||||
}
|
||||
n, _ := w.buf.Write(p[:toWrite])
|
||||
w.written += n
|
||||
p = p[n:]
|
||||
if w.written%ColumnsPerLine == 0 {
|
||||
w.buf.Write([]byte("\n"))
|
||||
}
|
||||
}
|
||||
if _, err := w.buf.WriteTo(w.dst); err != nil {
|
||||
// We always return n = 0 on error because it's hard to work back to the
|
||||
// input length that ended up written out. Not ideal, but Write errors
|
||||
// are not recoverable anyway.
|
||||
return 0, err
|
||||
}
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
// LastLineIsEmpty returns whether the last output line was empty, either
|
||||
// because no input was written, or because a multiple of BytesPerLine was.
|
||||
//
|
||||
// Calling LastLineIsEmpty before Close is meaningless.
|
||||
func (w *WrappedBase64Encoder) LastLineIsEmpty() bool {
|
||||
return w.written%ColumnsPerLine == 0
|
||||
}
|
||||
|
||||
const intro = "age-encryption.org/v1\n"
|
||||
|
||||
var stanzaPrefix = []byte("->")
|
||||
var footerPrefix = []byte("---")
|
||||
|
||||
func (r *Stanza) Marshal(w io.Writer) error {
|
||||
if _, err := w.Write(stanzaPrefix); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, a := range append([]string{r.Type}, r.Args...) {
|
||||
if _, err := io.WriteString(w, " "+a); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if _, err := io.WriteString(w, "\n"); err != nil {
|
||||
return err
|
||||
}
|
||||
ww := NewWrappedBase64Encoder(b64, w)
|
||||
if _, err := ww.Write(r.Body); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := ww.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
_, err := io.WriteString(w, "\n")
|
||||
return err
|
||||
}
|
||||
|
||||
func (h *Header) MarshalWithoutMAC(w io.Writer) error {
|
||||
if _, err := io.WriteString(w, intro); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, r := range h.Recipients {
|
||||
if err := r.Marshal(w); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
_, err := fmt.Fprintf(w, "%s", footerPrefix)
|
||||
return err
|
||||
}
|
||||
|
||||
func (h *Header) Marshal(w io.Writer) error {
|
||||
if err := h.MarshalWithoutMAC(w); err != nil {
|
||||
return err
|
||||
}
|
||||
mac := b64.EncodeToString(h.MAC)
|
||||
_, err := fmt.Fprintf(w, " %s\n", mac)
|
||||
return err
|
||||
}
|
||||
|
||||
type StanzaReader struct {
|
||||
r *bufio.Reader
|
||||
err error
|
||||
}
|
||||
|
||||
func NewStanzaReader(r *bufio.Reader) *StanzaReader {
|
||||
return &StanzaReader{r: r}
|
||||
}
|
||||
|
||||
func (r *StanzaReader) ReadStanza() (s *Stanza, err error) {
|
||||
// Read errors are unrecoverable.
|
||||
if r.err != nil {
|
||||
return nil, r.err
|
||||
}
|
||||
defer func() { r.err = err }()
|
||||
|
||||
s = &Stanza{}
|
||||
|
||||
line, err := r.r.ReadBytes('\n')
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read line: %w", err)
|
||||
}
|
||||
if !bytes.HasPrefix(line, stanzaPrefix) {
|
||||
return nil, fmt.Errorf("malformed stanza opening line: %q", line)
|
||||
}
|
||||
prefix, args := splitArgs(line)
|
||||
if prefix != string(stanzaPrefix) || len(args) < 1 {
|
||||
return nil, fmt.Errorf("malformed stanza: %q", line)
|
||||
}
|
||||
for _, a := range args {
|
||||
if !isValidString(a) {
|
||||
return nil, fmt.Errorf("malformed stanza: %q", line)
|
||||
}
|
||||
}
|
||||
s.Type = args[0]
|
||||
s.Args = args[1:]
|
||||
|
||||
for {
|
||||
line, err := r.r.ReadBytes('\n')
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read line: %w", err)
|
||||
}
|
||||
|
||||
b, err := DecodeString(strings.TrimSuffix(string(line), "\n"))
|
||||
if err != nil {
|
||||
if bytes.HasPrefix(line, footerPrefix) || bytes.HasPrefix(line, stanzaPrefix) {
|
||||
return nil, fmt.Errorf("malformed body line %q: stanza ended without a short line\nNote: this might be a file encrypted with an old beta version of age or rage. Use age v1.0.0-beta6 or rage to decrypt it.", line)
|
||||
}
|
||||
return nil, errorf("malformed body line %q: %v", line, err)
|
||||
}
|
||||
if len(b) > BytesPerLine {
|
||||
return nil, errorf("malformed body line %q: too long", line)
|
||||
}
|
||||
s.Body = append(s.Body, b...)
|
||||
if len(b) < BytesPerLine {
|
||||
// A stanza body always ends with a short line.
|
||||
return s, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type ParseError struct {
|
||||
err error
|
||||
}
|
||||
|
||||
func (e *ParseError) Error() string {
|
||||
return "parsing age header: " + e.err.Error()
|
||||
}
|
||||
|
||||
func (e *ParseError) Unwrap() error {
|
||||
return e.err
|
||||
}
|
||||
|
||||
func errorf(format string, a ...interface{}) error {
|
||||
return &ParseError{fmt.Errorf(format, a...)}
|
||||
}
|
||||
|
||||
// Parse returns the header and a Reader that begins at the start of the
|
||||
// payload.
|
||||
func Parse(input io.Reader) (*Header, io.Reader, error) {
|
||||
h := &Header{}
|
||||
rr := bufio.NewReader(input)
|
||||
|
||||
line, err := rr.ReadString('\n')
|
||||
if err != nil {
|
||||
return nil, nil, errorf("failed to read intro: %w", err)
|
||||
}
|
||||
if line != intro {
|
||||
return nil, nil, errorf("unexpected intro: %q", line)
|
||||
}
|
||||
|
||||
sr := NewStanzaReader(rr)
|
||||
for {
|
||||
peek, err := rr.Peek(len(footerPrefix))
|
||||
if err != nil {
|
||||
return nil, nil, errorf("failed to read header: %w", err)
|
||||
}
|
||||
|
||||
if bytes.Equal(peek, footerPrefix) {
|
||||
line, err := rr.ReadBytes('\n')
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to read header: %w", err)
|
||||
}
|
||||
|
||||
prefix, args := splitArgs(line)
|
||||
if prefix != string(footerPrefix) || len(args) != 1 {
|
||||
return nil, nil, errorf("malformed closing line: %q", line)
|
||||
}
|
||||
h.MAC, err = DecodeString(args[0])
|
||||
if err != nil || len(h.MAC) != 32 {
|
||||
return nil, nil, errorf("malformed closing line %q: %v", line, err)
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
s, err := sr.ReadStanza()
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to parse header: %w", err)
|
||||
}
|
||||
h.Recipients = append(h.Recipients, s)
|
||||
}
|
||||
|
||||
// If input is a bufio.Reader, rr might be equal to input because
|
||||
// bufio.NewReader short-circuits. In this case we can just return it (and
|
||||
// we would end up reading the buffer twice if we prepended the peek below).
|
||||
if rr == input {
|
||||
return h, rr, nil
|
||||
}
|
||||
// Otherwise, unwind the bufio overread and return the unbuffered input.
|
||||
buf, err := rr.Peek(rr.Buffered())
|
||||
if err != nil {
|
||||
return nil, nil, errorf("internal error: %v", err)
|
||||
}
|
||||
payload := io.MultiReader(bytes.NewReader(buf), input)
|
||||
return h, payload, nil
|
||||
}
|
||||
|
||||
func splitArgs(line []byte) (string, []string) {
|
||||
l := strings.TrimSuffix(string(line), "\n")
|
||||
parts := strings.Split(l, " ")
|
||||
return parts[0], parts[1:]
|
||||
}
|
||||
|
||||
func isValidString(s string) bool {
|
||||
if len(s) == 0 {
|
||||
return false
|
||||
}
|
||||
for _, c := range s {
|
||||
if c < 33 || c > 126 {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
+231
@@ -0,0 +1,231 @@
|
||||
// Copyright 2019 The age Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package stream implements a variant of the STREAM chunked encryption scheme.
|
||||
package stream
|
||||
|
||||
import (
|
||||
"crypto/cipher"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"golang.org/x/crypto/chacha20poly1305"
|
||||
"golang.org/x/crypto/poly1305"
|
||||
)
|
||||
|
||||
const ChunkSize = 64 * 1024
|
||||
|
||||
type Reader struct {
|
||||
a cipher.AEAD
|
||||
src io.Reader
|
||||
|
||||
unread []byte // decrypted but unread data, backed by buf
|
||||
buf [encChunkSize]byte
|
||||
|
||||
err error
|
||||
nonce [chacha20poly1305.NonceSize]byte
|
||||
}
|
||||
|
||||
const (
|
||||
encChunkSize = ChunkSize + poly1305.TagSize
|
||||
lastChunkFlag = 0x01
|
||||
)
|
||||
|
||||
func NewReader(key []byte, src io.Reader) (*Reader, error) {
|
||||
aead, err := chacha20poly1305.New(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Reader{
|
||||
a: aead,
|
||||
src: src,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r *Reader) Read(p []byte) (int, error) {
|
||||
if len(r.unread) > 0 {
|
||||
n := copy(p, r.unread)
|
||||
r.unread = r.unread[n:]
|
||||
return n, nil
|
||||
}
|
||||
if r.err != nil {
|
||||
return 0, r.err
|
||||
}
|
||||
if len(p) == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
last, err := r.readChunk()
|
||||
if err != nil {
|
||||
r.err = err
|
||||
return 0, err
|
||||
}
|
||||
|
||||
n := copy(p, r.unread)
|
||||
r.unread = r.unread[n:]
|
||||
|
||||
if last {
|
||||
// Ensure there is an EOF after the last chunk as expected. In other
|
||||
// words, check for trailing data after a full-length final chunk.
|
||||
// Hopefully, the underlying reader supports returning EOF even if it
|
||||
// had previously returned an EOF to ReadFull.
|
||||
if _, err := r.src.Read(make([]byte, 1)); err == nil {
|
||||
r.err = errors.New("trailing data after end of encrypted file")
|
||||
} else if err != io.EOF {
|
||||
r.err = fmt.Errorf("non-EOF error reading after end of encrypted file: %w", err)
|
||||
} else {
|
||||
r.err = io.EOF
|
||||
}
|
||||
}
|
||||
|
||||
return n, nil
|
||||
}
|
||||
|
||||
// readChunk reads the next chunk of ciphertext from r.src and makes it available
|
||||
// in r.unread. last is true if the chunk was marked as the end of the message.
|
||||
// readChunk must not be called again after returning a last chunk or an error.
|
||||
func (r *Reader) readChunk() (last bool, err error) {
|
||||
if len(r.unread) != 0 {
|
||||
panic("stream: internal error: readChunk called with dirty buffer")
|
||||
}
|
||||
|
||||
in := r.buf[:]
|
||||
n, err := io.ReadFull(r.src, in)
|
||||
switch {
|
||||
case err == io.EOF:
|
||||
// A message can't end without a marked chunk. This message is truncated.
|
||||
return false, io.ErrUnexpectedEOF
|
||||
case err == io.ErrUnexpectedEOF:
|
||||
// The last chunk can be short, but not empty unless it's the first and
|
||||
// only chunk.
|
||||
if !nonceIsZero(&r.nonce) && n == r.a.Overhead() {
|
||||
return false, errors.New("last chunk is empty, try age v1.0.0, and please consider reporting this")
|
||||
}
|
||||
in = in[:n]
|
||||
last = true
|
||||
setLastChunkFlag(&r.nonce)
|
||||
case err != nil:
|
||||
return false, err
|
||||
}
|
||||
|
||||
outBuf := make([]byte, 0, ChunkSize)
|
||||
out, err := r.a.Open(outBuf, r.nonce[:], in, nil)
|
||||
if err != nil && !last {
|
||||
// Check if this was a full-length final chunk.
|
||||
last = true
|
||||
setLastChunkFlag(&r.nonce)
|
||||
out, err = r.a.Open(outBuf, r.nonce[:], in, nil)
|
||||
}
|
||||
if err != nil {
|
||||
return false, errors.New("failed to decrypt and authenticate payload chunk")
|
||||
}
|
||||
|
||||
incNonce(&r.nonce)
|
||||
r.unread = r.buf[:copy(r.buf[:], out)]
|
||||
return last, nil
|
||||
}
|
||||
|
||||
func incNonce(nonce *[chacha20poly1305.NonceSize]byte) {
|
||||
for i := len(nonce) - 2; i >= 0; i-- {
|
||||
nonce[i]++
|
||||
if nonce[i] != 0 {
|
||||
break
|
||||
} else if i == 0 {
|
||||
// The counter is 88 bits, this is unreachable.
|
||||
panic("stream: chunk counter wrapped around")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func setLastChunkFlag(nonce *[chacha20poly1305.NonceSize]byte) {
|
||||
nonce[len(nonce)-1] = lastChunkFlag
|
||||
}
|
||||
|
||||
func nonceIsZero(nonce *[chacha20poly1305.NonceSize]byte) bool {
|
||||
return *nonce == [chacha20poly1305.NonceSize]byte{}
|
||||
}
|
||||
|
||||
type Writer struct {
|
||||
a cipher.AEAD
|
||||
dst io.Writer
|
||||
unwritten []byte // backed by buf
|
||||
buf [encChunkSize]byte
|
||||
nonce [chacha20poly1305.NonceSize]byte
|
||||
err error
|
||||
}
|
||||
|
||||
func NewWriter(key []byte, dst io.Writer) (*Writer, error) {
|
||||
aead, err := chacha20poly1305.New(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
w := &Writer{
|
||||
a: aead,
|
||||
dst: dst,
|
||||
}
|
||||
w.unwritten = w.buf[:0]
|
||||
return w, nil
|
||||
}
|
||||
|
||||
func (w *Writer) Write(p []byte) (n int, err error) {
|
||||
// TODO: consider refactoring with a bytes.Buffer.
|
||||
if w.err != nil {
|
||||
return 0, w.err
|
||||
}
|
||||
if len(p) == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
total := len(p)
|
||||
for len(p) > 0 {
|
||||
freeBuf := w.buf[len(w.unwritten):ChunkSize]
|
||||
n := copy(freeBuf, p)
|
||||
p = p[n:]
|
||||
w.unwritten = w.unwritten[:len(w.unwritten)+n]
|
||||
|
||||
if len(w.unwritten) == ChunkSize && len(p) > 0 {
|
||||
if err := w.flushChunk(notLastChunk); err != nil {
|
||||
w.err = err
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
}
|
||||
return total, nil
|
||||
}
|
||||
|
||||
// Close flushes the last chunk. It does not close the underlying Writer.
|
||||
func (w *Writer) Close() error {
|
||||
if w.err != nil {
|
||||
return w.err
|
||||
}
|
||||
|
||||
w.err = w.flushChunk(lastChunk)
|
||||
if w.err != nil {
|
||||
return w.err
|
||||
}
|
||||
|
||||
w.err = errors.New("stream.Writer is already closed")
|
||||
return nil
|
||||
}
|
||||
|
||||
const (
|
||||
lastChunk = true
|
||||
notLastChunk = false
|
||||
)
|
||||
|
||||
func (w *Writer) flushChunk(last bool) error {
|
||||
if !last && len(w.unwritten) != ChunkSize {
|
||||
panic("stream: internal error: flush called with partial chunk")
|
||||
}
|
||||
|
||||
if last {
|
||||
setLastChunkFlag(&w.nonce)
|
||||
}
|
||||
buf := w.a.Seal(w.buf[:0], w.nonce[:], w.unwritten, nil)
|
||||
_, err := w.dst.Write(buf)
|
||||
w.unwritten = w.buf[:0]
|
||||
incNonce(&w.nonce)
|
||||
return err
|
||||
}
|
||||
+84
@@ -0,0 +1,84 @@
|
||||
// Copyright 2021 The age Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package age
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ParseIdentities parses a file with one or more private key encodings, one per
|
||||
// line. Empty lines and lines starting with "#" are ignored.
|
||||
//
|
||||
// This is the same syntax as the private key files accepted by the CLI, except
|
||||
// the CLI also accepts SSH private keys, which are not recommended for the
|
||||
// average application.
|
||||
//
|
||||
// Currently, all returned values are of type *X25519Identity, but different
|
||||
// types might be returned in the future.
|
||||
func ParseIdentities(f io.Reader) ([]Identity, error) {
|
||||
const privateKeySizeLimit = 1 << 24 // 16 MiB
|
||||
var ids []Identity
|
||||
scanner := bufio.NewScanner(io.LimitReader(f, privateKeySizeLimit))
|
||||
var n int
|
||||
for scanner.Scan() {
|
||||
n++
|
||||
line := scanner.Text()
|
||||
if strings.HasPrefix(line, "#") || line == "" {
|
||||
continue
|
||||
}
|
||||
i, err := ParseX25519Identity(line)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error at line %d: %v", n, err)
|
||||
}
|
||||
ids = append(ids, i)
|
||||
}
|
||||
if err := scanner.Err(); err != nil {
|
||||
return nil, fmt.Errorf("failed to read secret keys file: %v", err)
|
||||
}
|
||||
if len(ids) == 0 {
|
||||
return nil, fmt.Errorf("no secret keys found")
|
||||
}
|
||||
return ids, nil
|
||||
}
|
||||
|
||||
// ParseRecipients parses a file with one or more public key encodings, one per
|
||||
// line. Empty lines and lines starting with "#" are ignored.
|
||||
//
|
||||
// This is the same syntax as the recipients files accepted by the CLI, except
|
||||
// the CLI also accepts SSH recipients, which are not recommended for the
|
||||
// average application.
|
||||
//
|
||||
// Currently, all returned values are of type *X25519Recipient, but different
|
||||
// types might be returned in the future.
|
||||
func ParseRecipients(f io.Reader) ([]Recipient, error) {
|
||||
const recipientFileSizeLimit = 1 << 24 // 16 MiB
|
||||
var recs []Recipient
|
||||
scanner := bufio.NewScanner(io.LimitReader(f, recipientFileSizeLimit))
|
||||
var n int
|
||||
for scanner.Scan() {
|
||||
n++
|
||||
line := scanner.Text()
|
||||
if strings.HasPrefix(line, "#") || line == "" {
|
||||
continue
|
||||
}
|
||||
r, err := ParseX25519Recipient(line)
|
||||
if err != nil {
|
||||
// Hide the error since it might unintentionally leak the contents
|
||||
// of confidential files.
|
||||
return nil, fmt.Errorf("malformed recipient at line %d", n)
|
||||
}
|
||||
recs = append(recs, r)
|
||||
}
|
||||
if err := scanner.Err(); err != nil {
|
||||
return nil, fmt.Errorf("failed to read recipients file: %v", err)
|
||||
}
|
||||
if len(recs) == 0 {
|
||||
return nil, fmt.Errorf("no recipients found")
|
||||
}
|
||||
return recs, nil
|
||||
}
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
// Copyright 2019 The age Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package age
|
||||
|
||||
import (
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"errors"
|
||||
"io"
|
||||
|
||||
"filippo.io/age/internal/format"
|
||||
"golang.org/x/crypto/chacha20poly1305"
|
||||
"golang.org/x/crypto/hkdf"
|
||||
)
|
||||
|
||||
// aeadEncrypt encrypts a message with a one-time key.
|
||||
func aeadEncrypt(key, plaintext []byte) ([]byte, error) {
|
||||
aead, err := chacha20poly1305.New(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// The nonce is fixed because this function is only used in places where the
|
||||
// spec guarantees each key is only used once (by deriving it from values
|
||||
// that include fresh randomness), allowing us to save the overhead.
|
||||
// For the code that encrypts the actual payload, look at the
|
||||
// filippo.io/age/internal/stream package.
|
||||
nonce := make([]byte, chacha20poly1305.NonceSize)
|
||||
return aead.Seal(nil, nonce, plaintext, nil), nil
|
||||
}
|
||||
|
||||
var errIncorrectCiphertextSize = errors.New("encrypted value has unexpected length")
|
||||
|
||||
// aeadDecrypt decrypts a message of an expected fixed size.
|
||||
//
|
||||
// The message size is limited to mitigate multi-key attacks, where a ciphertext
|
||||
// can be crafted that decrypts successfully under multiple keys. Short
|
||||
// ciphertexts can only target two keys, which has limited impact.
|
||||
func aeadDecrypt(key []byte, size int, ciphertext []byte) ([]byte, error) {
|
||||
aead, err := chacha20poly1305.New(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(ciphertext) != size+aead.Overhead() {
|
||||
return nil, errIncorrectCiphertextSize
|
||||
}
|
||||
nonce := make([]byte, chacha20poly1305.NonceSize)
|
||||
return aead.Open(nil, nonce, ciphertext, nil)
|
||||
}
|
||||
|
||||
func headerMAC(fileKey []byte, hdr *format.Header) ([]byte, error) {
|
||||
h := hkdf.New(sha256.New, fileKey, nil, []byte("header"))
|
||||
hmacKey := make([]byte, 32)
|
||||
if _, err := io.ReadFull(h, hmacKey); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
hh := hmac.New(sha256.New, hmacKey)
|
||||
if err := hdr.MarshalWithoutMAC(hh); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return hh.Sum(nil), nil
|
||||
}
|
||||
|
||||
func streamKey(fileKey, nonce []byte) []byte {
|
||||
h := hkdf.New(sha256.New, fileKey, nonce, []byte("payload"))
|
||||
streamKey := make([]byte, chacha20poly1305.KeySize)
|
||||
if _, err := io.ReadFull(h, streamKey); err != nil {
|
||||
panic("age: internal error: failed to read from HKDF: " + err.Error())
|
||||
}
|
||||
return streamKey
|
||||
}
|
||||
+182
@@ -0,0 +1,182 @@
|
||||
// Copyright 2019 The age Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package age
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"errors"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strconv"
|
||||
|
||||
"filippo.io/age/internal/format"
|
||||
"golang.org/x/crypto/chacha20poly1305"
|
||||
"golang.org/x/crypto/scrypt"
|
||||
)
|
||||
|
||||
const scryptLabel = "age-encryption.org/v1/scrypt"
|
||||
|
||||
// ScryptRecipient is a password-based recipient. Anyone with the password can
|
||||
// decrypt the message.
|
||||
//
|
||||
// If a ScryptRecipient is used, it must be the only recipient for the file: it
|
||||
// can't be mixed with other recipient types and can't be used multiple times
|
||||
// for the same file.
|
||||
//
|
||||
// Its use is not recommended for automated systems, which should prefer
|
||||
// X25519Recipient.
|
||||
type ScryptRecipient struct {
|
||||
password []byte
|
||||
workFactor int
|
||||
}
|
||||
|
||||
var _ Recipient = &ScryptRecipient{}
|
||||
|
||||
// NewScryptRecipient returns a new ScryptRecipient with the provided password.
|
||||
func NewScryptRecipient(password string) (*ScryptRecipient, error) {
|
||||
if len(password) == 0 {
|
||||
return nil, errors.New("passphrase can't be empty")
|
||||
}
|
||||
r := &ScryptRecipient{
|
||||
password: []byte(password),
|
||||
// TODO: automatically scale this to 1s (with a min) in the CLI.
|
||||
workFactor: 18, // 1s on a modern machine
|
||||
}
|
||||
return r, nil
|
||||
}
|
||||
|
||||
// SetWorkFactor sets the scrypt work factor to 2^logN.
|
||||
// It must be called before Wrap.
|
||||
//
|
||||
// If SetWorkFactor is not called, a reasonable default is used.
|
||||
func (r *ScryptRecipient) SetWorkFactor(logN int) {
|
||||
if logN > 30 || logN < 1 {
|
||||
panic("age: SetWorkFactor called with illegal value")
|
||||
}
|
||||
r.workFactor = logN
|
||||
}
|
||||
|
||||
const scryptSaltSize = 16
|
||||
|
||||
func (r *ScryptRecipient) Wrap(fileKey []byte) ([]*Stanza, error) {
|
||||
salt := make([]byte, scryptSaltSize)
|
||||
if _, err := rand.Read(salt[:]); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
logN := r.workFactor
|
||||
l := &Stanza{
|
||||
Type: "scrypt",
|
||||
Args: []string{format.EncodeToString(salt), strconv.Itoa(logN)},
|
||||
}
|
||||
|
||||
salt = append([]byte(scryptLabel), salt...)
|
||||
k, err := scrypt.Key(r.password, salt, 1<<logN, 8, 1, chacha20poly1305.KeySize)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to generate scrypt hash: %v", err)
|
||||
}
|
||||
|
||||
wrappedKey, err := aeadEncrypt(k, fileKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
l.Body = wrappedKey
|
||||
|
||||
return []*Stanza{l}, nil
|
||||
}
|
||||
|
||||
// ScryptIdentity is a password-based identity.
|
||||
type ScryptIdentity struct {
|
||||
password []byte
|
||||
maxWorkFactor int
|
||||
}
|
||||
|
||||
var _ Identity = &ScryptIdentity{}
|
||||
|
||||
// NewScryptIdentity returns a new ScryptIdentity with the provided password.
|
||||
func NewScryptIdentity(password string) (*ScryptIdentity, error) {
|
||||
if len(password) == 0 {
|
||||
return nil, errors.New("passphrase can't be empty")
|
||||
}
|
||||
i := &ScryptIdentity{
|
||||
password: []byte(password),
|
||||
maxWorkFactor: 22, // 15s on a modern machine
|
||||
}
|
||||
return i, nil
|
||||
}
|
||||
|
||||
// SetMaxWorkFactor sets the maximum accepted scrypt work factor to 2^logN.
|
||||
// It must be called before Unwrap.
|
||||
//
|
||||
// This caps the amount of work that Decrypt might have to do to process
|
||||
// received files. If SetMaxWorkFactor is not called, a fairly high default is
|
||||
// used, which might not be suitable for systems processing untrusted files.
|
||||
func (i *ScryptIdentity) SetMaxWorkFactor(logN int) {
|
||||
if logN > 30 || logN < 1 {
|
||||
panic("age: SetMaxWorkFactor called with illegal value")
|
||||
}
|
||||
i.maxWorkFactor = logN
|
||||
}
|
||||
|
||||
func (i *ScryptIdentity) Unwrap(stanzas []*Stanza) ([]byte, error) {
|
||||
for _, s := range stanzas {
|
||||
if s.Type == "scrypt" && len(stanzas) != 1 {
|
||||
return nil, errors.New("an scrypt recipient must be the only one")
|
||||
}
|
||||
}
|
||||
return multiUnwrap(i.unwrap, stanzas)
|
||||
}
|
||||
|
||||
var digitsRe = regexp.MustCompile(`^[1-9][0-9]*$`)
|
||||
|
||||
func (i *ScryptIdentity) unwrap(block *Stanza) ([]byte, error) {
|
||||
if block.Type != "scrypt" {
|
||||
return nil, ErrIncorrectIdentity
|
||||
}
|
||||
if len(block.Args) != 2 {
|
||||
return nil, errors.New("invalid scrypt recipient block")
|
||||
}
|
||||
salt, err := format.DecodeString(block.Args[0])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse scrypt salt: %v", err)
|
||||
}
|
||||
if len(salt) != scryptSaltSize {
|
||||
return nil, errors.New("invalid scrypt recipient block")
|
||||
}
|
||||
if w := block.Args[1]; !digitsRe.MatchString(w) {
|
||||
return nil, fmt.Errorf("scrypt work factor encoding invalid: %q", w)
|
||||
}
|
||||
logN, err := strconv.Atoi(block.Args[1])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse scrypt work factor: %v", err)
|
||||
}
|
||||
if logN > i.maxWorkFactor {
|
||||
return nil, fmt.Errorf("scrypt work factor too large: %v", logN)
|
||||
}
|
||||
if logN <= 0 { // unreachable
|
||||
return nil, fmt.Errorf("invalid scrypt work factor: %v", logN)
|
||||
}
|
||||
|
||||
salt = append([]byte(scryptLabel), salt...)
|
||||
k, err := scrypt.Key(i.password, salt, 1<<logN, 8, 1, chacha20poly1305.KeySize)
|
||||
if err != nil { // unreachable
|
||||
return nil, fmt.Errorf("failed to generate scrypt hash: %v", err)
|
||||
}
|
||||
|
||||
// This AEAD is not robust, so an attacker could craft a message that
|
||||
// decrypts under two different keys (meaning two different passphrases) and
|
||||
// then use an error side-channel in an online decryption oracle to learn if
|
||||
// either key is correct. This is deemed acceptable because the use case (an
|
||||
// online decryption oracle) is not recommended, and the security loss is
|
||||
// only one bit. This also does not bypass any scrypt work, although that work
|
||||
// can be precomputed in an online oracle scenario.
|
||||
fileKey, err := aeadDecrypt(k, fileKeySize, block.Body)
|
||||
if err == errIncorrectCiphertextSize {
|
||||
return nil, errors.New("invalid scrypt recipient block: incorrect file key size")
|
||||
} else if err != nil {
|
||||
return nil, ErrIncorrectIdentity
|
||||
}
|
||||
return fileKey, nil
|
||||
}
|
||||
+208
@@ -0,0 +1,208 @@
|
||||
// Copyright 2019 The age Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package age
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"filippo.io/age/internal/bech32"
|
||||
"filippo.io/age/internal/format"
|
||||
"golang.org/x/crypto/chacha20poly1305"
|
||||
"golang.org/x/crypto/curve25519"
|
||||
"golang.org/x/crypto/hkdf"
|
||||
)
|
||||
|
||||
const x25519Label = "age-encryption.org/v1/X25519"
|
||||
|
||||
// X25519Recipient is the standard age public key. Messages encrypted to this
|
||||
// recipient can be decrypted with the corresponding X25519Identity.
|
||||
//
|
||||
// This recipient is anonymous, in the sense that an attacker can't tell from
|
||||
// the message alone if it is encrypted to a certain recipient.
|
||||
type X25519Recipient struct {
|
||||
theirPublicKey []byte
|
||||
}
|
||||
|
||||
var _ Recipient = &X25519Recipient{}
|
||||
|
||||
// newX25519RecipientFromPoint returns a new X25519Recipient from a raw Curve25519 point.
|
||||
func newX25519RecipientFromPoint(publicKey []byte) (*X25519Recipient, error) {
|
||||
if len(publicKey) != curve25519.PointSize {
|
||||
return nil, errors.New("invalid X25519 public key")
|
||||
}
|
||||
r := &X25519Recipient{
|
||||
theirPublicKey: make([]byte, curve25519.PointSize),
|
||||
}
|
||||
copy(r.theirPublicKey, publicKey)
|
||||
return r, nil
|
||||
}
|
||||
|
||||
// ParseX25519Recipient returns a new X25519Recipient from a Bech32 public key
|
||||
// encoding with the "age1" prefix.
|
||||
func ParseX25519Recipient(s string) (*X25519Recipient, error) {
|
||||
t, k, err := bech32.Decode(s)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("malformed recipient %q: %v", s, err)
|
||||
}
|
||||
if t != "age" {
|
||||
return nil, fmt.Errorf("malformed recipient %q: invalid type %q", s, t)
|
||||
}
|
||||
r, err := newX25519RecipientFromPoint(k)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("malformed recipient %q: %v", s, err)
|
||||
}
|
||||
return r, nil
|
||||
}
|
||||
|
||||
func (r *X25519Recipient) Wrap(fileKey []byte) ([]*Stanza, error) {
|
||||
ephemeral := make([]byte, curve25519.ScalarSize)
|
||||
if _, err := rand.Read(ephemeral); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ourPublicKey, err := curve25519.X25519(ephemeral, curve25519.Basepoint)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sharedSecret, err := curve25519.X25519(ephemeral, r.theirPublicKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
l := &Stanza{
|
||||
Type: "X25519",
|
||||
Args: []string{format.EncodeToString(ourPublicKey)},
|
||||
}
|
||||
|
||||
salt := make([]byte, 0, len(ourPublicKey)+len(r.theirPublicKey))
|
||||
salt = append(salt, ourPublicKey...)
|
||||
salt = append(salt, r.theirPublicKey...)
|
||||
h := hkdf.New(sha256.New, sharedSecret, salt, []byte(x25519Label))
|
||||
wrappingKey := make([]byte, chacha20poly1305.KeySize)
|
||||
if _, err := io.ReadFull(h, wrappingKey); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
wrappedKey, err := aeadEncrypt(wrappingKey, fileKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
l.Body = wrappedKey
|
||||
|
||||
return []*Stanza{l}, nil
|
||||
}
|
||||
|
||||
// String returns the Bech32 public key encoding of r.
|
||||
func (r *X25519Recipient) String() string {
|
||||
s, _ := bech32.Encode("age", r.theirPublicKey)
|
||||
return s
|
||||
}
|
||||
|
||||
// X25519Identity is the standard age private key, which can decrypt messages
|
||||
// encrypted to the corresponding X25519Recipient.
|
||||
type X25519Identity struct {
|
||||
secretKey, ourPublicKey []byte
|
||||
}
|
||||
|
||||
var _ Identity = &X25519Identity{}
|
||||
|
||||
// newX25519IdentityFromScalar returns a new X25519Identity from a raw Curve25519 scalar.
|
||||
func newX25519IdentityFromScalar(secretKey []byte) (*X25519Identity, error) {
|
||||
if len(secretKey) != curve25519.ScalarSize {
|
||||
return nil, errors.New("invalid X25519 secret key")
|
||||
}
|
||||
i := &X25519Identity{
|
||||
secretKey: make([]byte, curve25519.ScalarSize),
|
||||
}
|
||||
copy(i.secretKey, secretKey)
|
||||
i.ourPublicKey, _ = curve25519.X25519(i.secretKey, curve25519.Basepoint)
|
||||
return i, nil
|
||||
}
|
||||
|
||||
// GenerateX25519Identity randomly generates a new X25519Identity.
|
||||
func GenerateX25519Identity() (*X25519Identity, error) {
|
||||
secretKey := make([]byte, curve25519.ScalarSize)
|
||||
if _, err := rand.Read(secretKey); err != nil {
|
||||
return nil, fmt.Errorf("internal error: %v", err)
|
||||
}
|
||||
return newX25519IdentityFromScalar(secretKey)
|
||||
}
|
||||
|
||||
// ParseX25519Identity returns a new X25519Identity from a Bech32 private key
|
||||
// encoding with the "AGE-SECRET-KEY-1" prefix.
|
||||
func ParseX25519Identity(s string) (*X25519Identity, error) {
|
||||
t, k, err := bech32.Decode(s)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("malformed secret key: %v", err)
|
||||
}
|
||||
if t != "AGE-SECRET-KEY-" {
|
||||
return nil, fmt.Errorf("malformed secret key: unknown type %q", t)
|
||||
}
|
||||
r, err := newX25519IdentityFromScalar(k)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("malformed secret key: %v", err)
|
||||
}
|
||||
return r, nil
|
||||
}
|
||||
|
||||
func (i *X25519Identity) Unwrap(stanzas []*Stanza) ([]byte, error) {
|
||||
return multiUnwrap(i.unwrap, stanzas)
|
||||
}
|
||||
|
||||
func (i *X25519Identity) unwrap(block *Stanza) ([]byte, error) {
|
||||
if block.Type != "X25519" {
|
||||
return nil, ErrIncorrectIdentity
|
||||
}
|
||||
if len(block.Args) != 1 {
|
||||
return nil, errors.New("invalid X25519 recipient block")
|
||||
}
|
||||
publicKey, err := format.DecodeString(block.Args[0])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse X25519 recipient: %v", err)
|
||||
}
|
||||
if len(publicKey) != curve25519.PointSize {
|
||||
return nil, errors.New("invalid X25519 recipient block")
|
||||
}
|
||||
|
||||
sharedSecret, err := curve25519.X25519(i.secretKey, publicKey)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid X25519 recipient: %v", err)
|
||||
}
|
||||
|
||||
salt := make([]byte, 0, len(publicKey)+len(i.ourPublicKey))
|
||||
salt = append(salt, publicKey...)
|
||||
salt = append(salt, i.ourPublicKey...)
|
||||
h := hkdf.New(sha256.New, sharedSecret, salt, []byte(x25519Label))
|
||||
wrappingKey := make([]byte, chacha20poly1305.KeySize)
|
||||
if _, err := io.ReadFull(h, wrappingKey); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fileKey, err := aeadDecrypt(wrappingKey, fileKeySize, block.Body)
|
||||
if err == errIncorrectCiphertextSize {
|
||||
return nil, errors.New("invalid X25519 recipient block: incorrect file key size")
|
||||
} else if err != nil {
|
||||
return nil, ErrIncorrectIdentity
|
||||
}
|
||||
return fileKey, nil
|
||||
}
|
||||
|
||||
// Recipient returns the public X25519Recipient value corresponding to i.
|
||||
func (i *X25519Identity) Recipient() *X25519Recipient {
|
||||
r := &X25519Recipient{}
|
||||
r.theirPublicKey = i.ourPublicKey
|
||||
return r
|
||||
}
|
||||
|
||||
// String returns the Bech32 private key encoding of i.
|
||||
func (i *X25519Identity) String() string {
|
||||
s, _ := bech32.Encode("AGE-SECRET-KEY-", i.secretKey)
|
||||
return strings.ToUpper(s)
|
||||
}
|
||||
+93
@@ -0,0 +1,93 @@
|
||||
// Copyright 2014 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package hkdf implements the HMAC-based Extract-and-Expand Key Derivation
|
||||
// Function (HKDF) as defined in RFC 5869.
|
||||
//
|
||||
// HKDF is a cryptographic key derivation function (KDF) with the goal of
|
||||
// expanding limited input keying material into one or more cryptographically
|
||||
// strong secret keys.
|
||||
package hkdf // import "golang.org/x/crypto/hkdf"
|
||||
|
||||
import (
|
||||
"crypto/hmac"
|
||||
"errors"
|
||||
"hash"
|
||||
"io"
|
||||
)
|
||||
|
||||
// Extract generates a pseudorandom key for use with Expand from an input secret
|
||||
// and an optional independent salt.
|
||||
//
|
||||
// Only use this function if you need to reuse the extracted key with multiple
|
||||
// Expand invocations and different context values. Most common scenarios,
|
||||
// including the generation of multiple keys, should use New instead.
|
||||
func Extract(hash func() hash.Hash, secret, salt []byte) []byte {
|
||||
if salt == nil {
|
||||
salt = make([]byte, hash().Size())
|
||||
}
|
||||
extractor := hmac.New(hash, salt)
|
||||
extractor.Write(secret)
|
||||
return extractor.Sum(nil)
|
||||
}
|
||||
|
||||
type hkdf struct {
|
||||
expander hash.Hash
|
||||
size int
|
||||
|
||||
info []byte
|
||||
counter byte
|
||||
|
||||
prev []byte
|
||||
buf []byte
|
||||
}
|
||||
|
||||
func (f *hkdf) Read(p []byte) (int, error) {
|
||||
// Check whether enough data can be generated
|
||||
need := len(p)
|
||||
remains := len(f.buf) + int(255-f.counter+1)*f.size
|
||||
if remains < need {
|
||||
return 0, errors.New("hkdf: entropy limit reached")
|
||||
}
|
||||
// Read any leftover from the buffer
|
||||
n := copy(p, f.buf)
|
||||
p = p[n:]
|
||||
|
||||
// Fill the rest of the buffer
|
||||
for len(p) > 0 {
|
||||
f.expander.Reset()
|
||||
f.expander.Write(f.prev)
|
||||
f.expander.Write(f.info)
|
||||
f.expander.Write([]byte{f.counter})
|
||||
f.prev = f.expander.Sum(f.prev[:0])
|
||||
f.counter++
|
||||
|
||||
// Copy the new batch into p
|
||||
f.buf = f.prev
|
||||
n = copy(p, f.buf)
|
||||
p = p[n:]
|
||||
}
|
||||
// Save leftovers for next run
|
||||
f.buf = f.buf[n:]
|
||||
|
||||
return need, nil
|
||||
}
|
||||
|
||||
// Expand returns a Reader, from which keys can be read, using the given
|
||||
// pseudorandom key and optional context info, skipping the extraction step.
|
||||
//
|
||||
// The pseudorandomKey should have been generated by Extract, or be a uniformly
|
||||
// random or pseudorandom cryptographically strong key. See RFC 5869, Section
|
||||
// 3.3. Most common scenarios will want to use New instead.
|
||||
func Expand(hash func() hash.Hash, pseudorandomKey, info []byte) io.Reader {
|
||||
expander := hmac.New(hash, pseudorandomKey)
|
||||
return &hkdf{expander, expander.Size(), info, 1, nil, nil}
|
||||
}
|
||||
|
||||
// New returns a Reader, from which keys can be read, using the given hash,
|
||||
// secret, salt and context info. Salt and info can be nil.
|
||||
func New(hash func() hash.Hash, secret, salt, info []byte) io.Reader {
|
||||
prk := Extract(hash, secret, salt)
|
||||
return Expand(hash, prk, info)
|
||||
}
|
||||
Vendored
+7
@@ -1,3 +1,9 @@
|
||||
# filippo.io/age v1.1.1
|
||||
## explicit; go 1.19
|
||||
filippo.io/age
|
||||
filippo.io/age/internal/bech32
|
||||
filippo.io/age/internal/format
|
||||
filippo.io/age/internal/stream
|
||||
# github.com/AlecAivazis/survey/v2 v2.3.6
|
||||
## explicit; go 1.13
|
||||
github.com/AlecAivazis/survey/v2
|
||||
@@ -474,6 +480,7 @@ golang.org/x/crypto/chacha20poly1305
|
||||
golang.org/x/crypto/curve25519
|
||||
golang.org/x/crypto/curve25519/internal/field
|
||||
golang.org/x/crypto/ed25519
|
||||
golang.org/x/crypto/hkdf
|
||||
golang.org/x/crypto/internal/alias
|
||||
golang.org/x/crypto/internal/poly1305
|
||||
golang.org/x/crypto/md4
|
||||
|
||||
Reference in new issue
Block a user