Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 1 addition & 3 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ module github.com/cosi-project/runtime
go 1.26.5

require (
github.com/ProtonMail/gopenpgp/v2 v2.10.0
github.com/ProtonMail/gopenpgp/v3 v3.4.1
github.com/cenkalti/backoff/v4 v4.3.0
github.com/gertd/go-pluralize v0.2.1
github.com/grpc-ecosystem/grpc-gateway/v2 v2.30.0
Expand All @@ -27,11 +27,9 @@ require (

require (
github.com/ProtonMail/go-crypto v1.4.1 // indirect
github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f // indirect
github.com/cloudflare/circl v1.6.4 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/crypto v0.54.0 // indirect
Expand Down
43 changes: 4 additions & 39 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
github.com/ProtonMail/go-crypto v1.4.1 h1:9RfcZHqEQUvP8RzecWEUafnZVtEvrBVL9BiF67IQOfM=
github.com/ProtonMail/go-crypto v1.4.1/go.mod h1:e1OaTyu5SYVrO9gKOEhTc+5UcXtTUa+P3uLudwcgPqo=
github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f h1:tCbYj7/299ekTTXpdwKYF8eBlsYsDVoggDAuAjoK66k=
github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f/go.mod h1:gcr0kNtGBqin9zDW9GOHcVntrwnjrK+qdJ06mWYBybw=
github.com/ProtonMail/gopenpgp/v2 v2.10.0 h1:llCzLvntC9+iH+if/na4AgKTef/Zm4vpaRrR3+JdKvo=
github.com/ProtonMail/gopenpgp/v2 v2.10.0/go.mod h1:dc0h9Pg3ftfN0U4pfRzujilfh61A2R52wgMkZWcWm2I=
github.com/ProtonMail/gopenpgp/v3 v3.4.1 h1:K7uUhSHSJxORZ+RuHpilTT6S4MA2whCRlXNwLqd0+ys=
github.com/ProtonMail/gopenpgp/v3 v3.4.1/go.mod h1:bGdV9f6edhmd581wzXsQCTKdH8bXBbyhkgDKPjwPc6U=
github.com/brianvoe/gofakeit/v7 v7.7.3 h1:RWOATEGpJ5EVg2nN8nlaEyaV/aB4d6c3GqYrbqQekss=
github.com/brianvoe/gofakeit/v7 v7.7.3/go.mod h1:QXuPeBw164PJCzCUZVmgpgHJ3Llj49jSLVkKPMtxtxA=
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
Expand Down Expand Up @@ -35,12 +33,10 @@ github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+l
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
github.com/klauspost/compress v1.19.2 h1:hMRETovs/pu/dVWN7zIT1PGG8t509MwT6bO7XSi26R8=
github.com/klauspost/compress v1.19.2/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo=
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
Expand All @@ -55,7 +51,6 @@ github.com/siderolabs/protoenc v0.2.4 h1:D3Fpn2nQSQOhl8ZlAxijZAf7K6F8CM1uZq0afIG
github.com/siderolabs/protoenc v0.2.4/go.mod h1:i5XLHjfv5vyi7LhQrSEo19HCA+lYtDd7CWxsoWp9XE8=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.etcd.io/bbolt v1.5.0 h1:S7GAl7Fxv12yohbwFfIbQCGDWbQbtDGPET4P/bD4lxU=
go.etcd.io/bbolt v1.5.0/go.mod h1:mkltfYE5aUHQxUct9N9V+Kp7aSjFqjgrhcXIS70Lrdk=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
Expand All @@ -80,48 +75,18 @@ go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw=
go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg=
go.yaml.in/yaml/v4 v4.0.0-rc.6 h1:1h7H1ohdUh93/FyE4YaDa1Zh64K6VVbjF4K6WUxMtH4=
go.yaml.in/yaml/v4 v4.0.0-rc.6/go.mod h1:aZqd9kCMsGL7AuUv/m/PvWLdg5sjJsZ4oHDEnfPPfY0=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw=
golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE=
golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek=
golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs=
golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY=
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
google.golang.org/genproto/googleapis/api v0.0.0-20260803160001-6ac0973c030d h1:FarXi840EJWSHYTN3ERkADbPWjl307+FGrA22KAVjjc=
Expand Down
76 changes: 70 additions & 6 deletions pkg/keystorage/keystorage.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,20 @@ import (
"sort"
"sync"

"github.com/ProtonMail/gopenpgp/v2/helper"
"github.com/ProtonMail/gopenpgp/v3/crypto"
"github.com/siderolabs/gen/maps"
"github.com/siderolabs/gen/xerrors"

"github.com/cosi-project/runtime/api/key_storage"
)

// pgp returns the OpenPGP handle used to encrypt and decrypt the key slots,
// initializing it on the first use.
//
// The default profile is used, as it produces messages compatible with the ones
// previously written by gopenpgp/v2.
var pgp = sync.OnceValue(crypto.PGP)

// KeyStorage is a key storage that can be used to store and retrieve the master key.
//
//nolint:govet
Expand Down Expand Up @@ -61,7 +68,7 @@ func (ks *KeyStorage) Initialize(masterKey []byte, slotID, slotPublicKey string)
return xerrors.NewTaggedf[AlreadyInitializedTag]("key storage is already initialized")
}

encryptedSlot, err := helper.EncryptBinaryMessageArmored(slotPublicKey, masterKey)
encryptedSlot, err := encryptSlot(slotPublicKey, masterKey)
if err != nil {
return xerrors.NewTaggedf[KeyEncryptionFailureTag]("failed to encrypt slot '%s': %w", slotID, err)
}
Expand All @@ -70,7 +77,7 @@ func (ks *KeyStorage) Initialize(masterKey []byte, slotID, slotPublicKey string)
ks.underlying.KeySlots = map[string]*key_storage.KeySlot{
slotID: {
Algorithm: key_storage.Algorithm_PGP_AES_GCM_256,
EncryptedKey: []byte(encryptedSlot),
EncryptedKey: encryptedSlot,
},
}
ks.underlying.KeysHmacHash = ks.hashSlots(masterKey)
Expand Down Expand Up @@ -101,14 +108,14 @@ func (ks *KeyStorage) AddKeySlot(newSlotID, newSlotPublicKey, oldSlotID, oldSlot
return err
}

encryptedSlot, err := helper.EncryptBinaryMessageArmored(newSlotPublicKey, masterKey)
encryptedSlot, err := encryptSlot(newSlotPublicKey, masterKey)
if err != nil {
return xerrors.NewTaggedf[KeyEncryptionFailureTag]("failed to encrypt slot '%s': %w", newSlotID, err)
}

ks.underlying.GetKeySlots()[newSlotID] = &key_storage.KeySlot{
Algorithm: key_storage.Algorithm_PGP_AES_GCM_256,
EncryptedKey: []byte(encryptedSlot),
EncryptedKey: encryptedSlot,
}

ks.underlying.KeysHmacHash = ks.hashSlots(masterKey)
Expand Down Expand Up @@ -170,7 +177,7 @@ func (ks *KeyStorage) getKey(slotID string, slotPrivateKey string) ([]byte, erro
return nil, xerrors.NewTaggedf[AlgorithmMismatchTag]("slot '%s' algorithm mismatch", slotID)
}

masterKey, err := helper.DecryptBinaryMessageArmored(slotPrivateKey, nil, string(slot.EncryptedKey))
masterKey, err := decryptSlot(slotPrivateKey, slot.EncryptedKey)
if err != nil {
return nil, xerrors.NewTaggedf[KeyDecryptionFailureTag]("failed to decrypt slot '%s': %w", slotID, err)
}
Expand Down Expand Up @@ -233,6 +240,63 @@ func (ks *KeyStorage) hashSlots(masterKey []byte) []byte {
return hash.Sum(nil)
}

// encryptSlot encrypts the master key with the given armored key, and returns the armored ciphertext.
//
// The key might be either a public or a private key, in the latter case the public part is used.
func encryptSlot(slotPublicKey string, masterKey []byte) ([]byte, error) {
key, err := crypto.NewKeyFromArmored(slotPublicKey)
if err != nil {
return nil, fmt.Errorf("unable to load armored key: %w", err)
}

defer key.ClearPrivateParams()

if key.IsPrivate() {
if key, err = key.ToPublic(); err != nil {
return nil, fmt.Errorf("unable to extract public key from private key: %w", err)
}
}

encryption, err := pgp().Encryption().Recipient(key).New()
if err != nil {
return nil, fmt.Errorf("unable to create encryption handle: %w", err)
}

message, err := encryption.Encrypt(masterKey)
if err != nil {
return nil, fmt.Errorf("unable to encrypt message: %w", err)
}

armored, err := message.ArmorBytes()
if err != nil {
return nil, fmt.Errorf("unable to armor the ciphertext: %w", err)
}

return armored, nil
}

// decryptSlot decrypts the master key from the armored ciphertext with the given armored private key.
func decryptSlot(slotPrivateKey string, ciphertext []byte) ([]byte, error) {
key, err := crypto.NewPrivateKeyFromArmored(slotPrivateKey, nil)
if err != nil {
return nil, fmt.Errorf("unable to parse the private key: %w", err)
}

defer key.ClearPrivateParams()

decryption, err := pgp().Decryption().DecryptionKey(key).New()
if err != nil {
return nil, fmt.Errorf("unable to create decryption handle: %w", err)
}

result, err := decryption.Decrypt(ciphertext, crypto.Armor)
if err != nil {
return nil, fmt.Errorf("unable to decrypt message: %w", err)
}

return result.Bytes(), nil
}

func isZero(underlying *key_storage.Storage) bool {
return underlying.GetStorageVersion() == key_storage.StorageVersion_STORAGE_VERSION_UNSPECIFIED &&
len(underlying.GetKeySlots()) == 0 &&
Expand Down
24 changes: 21 additions & 3 deletions pkg/keystorage/keystorage_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,13 @@ var (
//go:embed testdata/private.key
privateKey string

//go:embed testdata/private.key
//go:embed testdata/public.key
publicKey string

// keyStorageV2 is a key storage marshaled by a version of this package built on top of gopenpgp/v2,
// it is used to verify that the key slots stay readable after the migration to gopenpgp/v3.
//go:embed testdata/keystorage-v2.bin
keyStorageV2 []byte
)

const (
Expand Down Expand Up @@ -131,6 +136,19 @@ func TestMarshalUnmarshal(t *testing.T) {
require.Equal(t, ksKey, key)
}

func TestUnmarshalGopenPGPV2(t *testing.T) {
t.Parallel()

var ks keystorage.KeyStorage

require.NoError(t, ks.UnmarshalBinary(keyStorageV2))
Comment thread
smira marked this conversation as resolved.

key, err := ks.GetMasterKey(slotID, privateKey)
require.NoError(t, err)

require.Equal(t, []byte(masterKey), key)
}

func TestKeyStorage_DeleteMasterKeySlot(t *testing.T) {
type args struct {
slotID string
Expand Down Expand Up @@ -270,7 +288,7 @@ func TestKeyStorage_Set(t *testing.T) {
slotPublicKey: publicKey[:32],
newSlotID: "new-slot-id",
},
testErr: check.ErrorTagIs[keystorage.KeyDecryptionFailureTag](),
testErr: check.ErrorTagIs[keystorage.KeyEncryptionFailureTag](),
},
"proper key": {
args: args{
Expand All @@ -290,7 +308,7 @@ func TestKeyStorage_Set(t *testing.T) {

require.NoError(t, ks.Initialize([]byte(masterKey), slotID, publicKey))

tt.testErr(t, ks.AddKeySlot(tt.args.newSlotID, tt.args.slotPublicKey, tt.args.slotID, tt.args.slotPublicKey))
tt.testErr(t, ks.AddKeySlot(tt.args.newSlotID, tt.args.slotPublicKey, tt.args.slotID, privateKey))
})
}
}
Expand Down
20 changes: 20 additions & 0 deletions pkg/keystorage/testdata/keystorage-v2.bin
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
�
slot-id��-----BEGIN PGP MESSAGE-----
Comment: https://gopenpgp.org
Version: GopenPGP 2.10.0

wcFMA0nxFfEdWM7ZARAAvNROO3B+DLoZS0zt3XaVjYCiDTR56pUKo8YjOGgGjLXP
ZtdH2ta6Yrwm+eJ40P+tTKpvNTz/hVSjX58LAQjjxJUXO2NrcmOwo2agZvwL/SyU
G5y0EzXixQoAOOj5ESYFwhO8gz0hAG5SQ8gEO191F8j021JCJl0jSvmZ6PRpd3kj
R+pfmmix13wstUe6ogCGpVZ4yElZBHg+LrR8K3Rm5zRD48K3nnWq08ASA2rTM7Zi
fzVhY8TJMPabg0wtW5mKK9aSclW7lnTjQfL6e9VvmaeMO+m9aziaOXB4E1v+WZUr
ldzs2alMdnzNx99xHuAZwVDQsZphN82D+VqZ/Xy7b71NLThDy6vWpZ1wrGKAOxnk
Dz3Wscsre3r5HeqccYmkUnwpTEoTQHWP2R2HweKvPbc1VPE7OMfhDoLBTiadM96H
U3WsXWQ80XsLHJvCq0UzIyGpRueVZbAG3waFbTqCN9iKrwspV8t1pMQfUHgNfmIZ
agIME1qN62G9Cz6RoBFdTk78/UHfedCdHnQfBfiD5pWm2aTu5WA8wbINmvfzB2lD
dkBUl3UHckcExofefKnn0rXFOJ2C0aES21mwjDE7OLdmk/Bf3YRZwicRNNGeCwZw
Z/BQXdorpGGDKexpr7JpVqHYfHoOrzt9yZGROtxREIg4OM2HfvveJXT8MpZXSzXS
UQHjJfVkviYs/LVJZuokiiTfrupG2JfeM8OJQr1yrQLtBfIMD9wZMfLonmjzt87g
osnXbMChAKT1328miq+6sJht+00z9KifGaIyBFOm3wLScw==
=hMP0
-----END PGP MESSAGE----- B�j]t�ʮ�Uι[����������������
Expand Down