diff --git a/abe/cpabe/tkn20/example_test.go b/abe/cpabe/tkn20/example_test.go index 1b263ff15..bd880333d 100644 --- a/abe/cpabe/tkn20/example_test.go +++ b/abe/cpabe/tkn20/example_test.go @@ -84,6 +84,8 @@ func Example() { if err != nil { log.Fatalf("%s", err) } + fmt.Printf("plaintext size: %v bytes\n", len(msgStr)) + fmt.Printf("ciphertext size: %v bytes\n", len(ct)) // generate secret key for certain set of attributes wrongAttrs := cpabe.Attributes{} @@ -127,6 +129,9 @@ func Example() { log.Fatalf("recovered plaintext: %s is not equal to original msg: %s", pt, msgStr) } fmt.Println("Successfully recovered plaintext") - // Output: (occupation:doctor and country:US) + // Output: + // (occupation:doctor and country:US) + // plaintext size: 27 bytes + // ciphertext size: 2747 bytes // Successfully recovered plaintext } diff --git a/abe/cpabe/tkn20/format_test.go b/abe/cpabe/tkn20/format_test.go index 122bf5e06..35b64e746 100644 --- a/abe/cpabe/tkn20/format_test.go +++ b/abe/cpabe/tkn20/format_test.go @@ -41,20 +41,53 @@ func TestAttributeKeyFormat(t *testing.T) { } } +func TestCiphertext_v137(t *testing.T) { + // As of v1.3.8 ciphertext format changed to use wider prefixes. + // Ciphertexts in the previous format are still decryptable. + // The following functions are backwards-compatible: + // - AttributeKey.Decrypt + // - Attributes.CouldDecrypt + // - Policy.ExtractFromCiphertext + testCiphertext(t, "testdata/ciphertext_v137") +} + func TestCiphertext(t *testing.T) { - ciphertext, err := os.ReadFile("testdata/ciphertext") + testCiphertext(t, "testdata/ciphertext") +} + +func testCiphertext(t *testing.T, ctName string) { + t.Logf("Checking ciphertext: %v\n", ctName) + ciphertext, err := os.ReadFile(ctName) if err != nil { t.Fatalf("Unable to read ciphertext data") } - policyKey, err := os.ReadFile("testdata/attributeKey") + attributeKey, err := os.ReadFile("testdata/attributeKey") if err != nil { t.Fatalf("Unable to read secret key") } sk := AttributeKey{} - err = sk.UnmarshalBinary(policyKey) + err = sk.UnmarshalBinary(attributeKey) if err != nil { t.Fatalf("unable to parse secret key") } + attrs := Attributes{} + attrs.FromMap(map[string]string{"country": "NL", "EU": "true"}) + if !attrs.CouldDecrypt(ciphertext) { + t.Fatal("these attributes will be unable to decrypt message") + } + policy := Policy{} + err = policy.FromString("EU: true") + if err != nil { + t.Fatal("error creating policy from string") + } + gotPolicy := new(Policy) + err = gotPolicy.ExtractFromCiphertext(ciphertext) + if err != nil { + t.Fatal("error extracting policy from ciphertext") + } + if !policy.Equal(gotPolicy) { + t.Fatal("ciphertext's policy mismatches the original policy") + } msg, err := sk.Decrypt(ciphertext) if err != nil { t.Fatal("unable to decrypt message") diff --git a/abe/cpabe/tkn20/gen_testdata.go b/abe/cpabe/tkn20/gen_testdata.go index 9629efcbc..41886e89f 100644 --- a/abe/cpabe/tkn20/gen_testdata.go +++ b/abe/cpabe/tkn20/gen_testdata.go @@ -6,11 +6,10 @@ package main import ( "encoding" - mrand "math/rand" "os" - "path/filepath" cpabe "github.com/cloudflare/circl/abe/cpabe/tkn20" + "github.com/cloudflare/circl/xof" ) func writeToFile(name string, data []byte) { @@ -30,13 +29,9 @@ func dumpToFile(name string, m encoding.BinaryMarshaler) { func main() { // Using fixed PRNG for reproducibility, - fixedSeed := int64(0xC1C1C1C1) - prng := mrand.New(mrand.NewSource(fixedSeed)) - if prng == nil { - panic("failed to create PRNG") - } + prng := xof.SHAKE128.New() - err := os.MkdirAll(filepath.Join(".", "testdata"), 0o755) + err := os.MkdirAll("testdata", 0o755) if err != nil { panic(err) } diff --git a/abe/cpabe/tkn20/internal/tkn/bk.go b/abe/cpabe/tkn20/internal/tkn/bk.go index 1d3fc3657..c96e86d29 100644 --- a/abe/cpabe/tkn20/internal/tkn/bk.go +++ b/abe/cpabe/tkn20/internal/tkn/bk.go @@ -1,6 +1,7 @@ package tkn import ( + "bytes" "crypto/subtle" "fmt" "io" @@ -20,6 +21,9 @@ import ( // for our output size of 256 bits. const macKeySeedSize = 72 +// As of v1.3.8, ciphertexts are prefixed with this string. +const CiphertextVersion = "v1.3.8" + func blakeEncrypt(key []byte, msg []byte) ([]byte, error) { xof, err := blake2b.NewXOF(blake2b.OutputLengthUnknown, key) if err != nil { @@ -117,27 +121,39 @@ func EncryptCCA(rand io.Reader, public *PublicParams, policy *Policy, msg []byte if err != nil { return nil, err } - macData := appendLenPrefixed(nil, C1) - macData = appendLenPrefixed(macData, env) + macData := appendLen32Prefixed(nil, C1) + macData = appendLen32Prefixed(macData, env) tag, err := blakeMac(macKey, macData) if err != nil { return nil, err } - ret := appendLenPrefixed(nil, id) - ret = appendLenPrefixed(ret, macData) + ret := append([]byte{}, []byte(CiphertextVersion)...) + ret = appendLenPrefixed(ret, id) + ret = appendLen32Prefixed(ret, macData) ret = appendLenPrefixed(ret, tag) return ret, nil } +type rmLenPref = func([]byte) ([]byte, []byte, error) + +func checkCiphertextFormat(ciphertext []byte) (ct []byte, fn rmLenPref) { + const N = len(CiphertextVersion) + if bytes.Equal(ciphertext[0:N], []byte(CiphertextVersion)) { + return ciphertext[N:], removeLen32Prefixed + } + return ciphertext, removeLenPrefixed +} + func DecryptCCA(ciphertext []byte, key *AttributesKey) ([]byte, error) { - id, rest, err := removeLenPrefixed(ciphertext) + rest, removeLenPrefixedVar := checkCiphertextFormat(ciphertext) + id, rest, err := removeLenPrefixed(rest) if err != nil { return nil, err } - macData, rest, err := removeLenPrefixed(rest) + macData, rest, err := removeLenPrefixedVar(rest) if err != nil { return nil, err } @@ -145,11 +161,11 @@ func DecryptCCA(ciphertext []byte, key *AttributesKey) ([]byte, error) { if err != nil { return nil, err } - C1, envRaw, err := removeLenPrefixed(macData) + C1, envRaw, err := removeLenPrefixedVar(macData) if err != nil { return nil, err } - env, _, err := removeLenPrefixed(envRaw) + env, _, err := removeLenPrefixedVar(envRaw) if err != nil { return nil, err } @@ -208,15 +224,16 @@ func DecryptCCA(ciphertext []byte, key *AttributesKey) ([]byte, error) { } func CouldDecrypt(ciphertext []byte, a *Attributes) bool { - id, rest, err := removeLenPrefixed(ciphertext) + rest, removeLenPrefixedVar := checkCiphertextFormat(ciphertext) + id, rest, err := removeLenPrefixed(rest) if err != nil { return false } - macData, _, err := removeLenPrefixed(rest) + macData, _, err := removeLenPrefixedVar(rest) if err != nil { return false } - C1, _, err := removeLenPrefixed(macData) + C1, _, err := removeLenPrefixedVar(macData) if err != nil { return false } @@ -237,15 +254,16 @@ func CouldDecrypt(ciphertext []byte, a *Attributes) bool { } func (p *Policy) ExtractFromCiphertext(ct []byte) error { - _, rest, err := removeLenPrefixed(ct) + rest, removeLenPrefixedVar := checkCiphertextFormat(ct) + _, rest, err := removeLenPrefixed(rest) if err != nil { return fmt.Errorf("invalid ciphertext") } - macData, _, err := removeLenPrefixed(rest) + macData, _, err := removeLenPrefixedVar(rest) if err != nil { return fmt.Errorf("invalid ciphertext") } - C1, _, err := removeLenPrefixed(macData) + C1, _, err := removeLenPrefixedVar(macData) if err != nil { return fmt.Errorf("invalid ciphertext") } diff --git a/abe/cpabe/tkn20/internal/tkn/util.go b/abe/cpabe/tkn20/internal/tkn/util.go index 9afbe88a7..0c0f94f1c 100644 --- a/abe/cpabe/tkn20/internal/tkn/util.go +++ b/abe/cpabe/tkn20/internal/tkn/util.go @@ -42,14 +42,14 @@ func HashStringToScalar(key []byte, value string) *pairing.Scalar { return s } -func appendLenPrefixed(a []byte, b []byte) []byte { +func appendLen16Prefixed(a []byte, b []byte) []byte { a = append(a, 0, 0) binary.LittleEndian.PutUint16(a[len(a)-2:], uint16(len(b))) a = append(a, b...) return a } -func removeLenPrefixed(data []byte) (next []byte, remainder []byte, err error) { +func removeLen16Prefixed(data []byte) (next []byte, remainder []byte, err error) { if len(data) < 2 { return nil, nil, fmt.Errorf("data too short") } @@ -60,6 +60,29 @@ func removeLenPrefixed(data []byte) (next []byte, remainder []byte, err error) { return data[2 : 2+itemLen], data[2+itemLen:], nil } +var ( + appendLenPrefixed = appendLen16Prefixed + removeLenPrefixed = removeLen16Prefixed +) + +func appendLen32Prefixed(a []byte, b []byte) []byte { + a = append(a, 0, 0, 0, 0) + binary.LittleEndian.PutUint32(a[len(a)-4:], uint32(len(b))) + a = append(a, b...) + return a +} + +func removeLen32Prefixed(data []byte) (next []byte, remainder []byte, err error) { + if len(data) < 4 { + return nil, nil, fmt.Errorf("data too short") + } + itemLen := int(binary.LittleEndian.Uint32(data)) + if (4 + itemLen) > len(data) { + return nil, nil, fmt.Errorf("data too short") + } + return data[4 : 4+itemLen], data[4+itemLen:], nil +} + func marshalBinarySortedMapMatrixG1(m map[string]*matrixG1) ([]byte, error) { sortedKeys := make([]string, 0, len(m)) for key := range m { diff --git a/abe/cpabe/tkn20/longpt_test.go b/abe/cpabe/tkn20/longpt_test.go new file mode 100644 index 000000000..861e5facc --- /dev/null +++ b/abe/cpabe/tkn20/longpt_test.go @@ -0,0 +1,60 @@ +package tkn20_test + +import ( + "bytes" + "crypto/sha256" + "fmt" + "io" + "testing" + + cpabe "github.com/cloudflare/circl/abe/cpabe/tkn20" + "github.com/cloudflare/circl/internal/test" + "github.com/cloudflare/circl/xof" +) + +func TestLongPlaintext(t *testing.T) { + // Fixed PRNG for test reproducibility. + prng := xof.SHAKE128.New() + + pk, msk, err := cpabe.Setup(prng) + test.CheckNoErr(t, err, "setup failed") + + attrs := cpabe.Attributes{} + attrs.FromMap(map[string]string{ + "occupation": "doctor", + "country": "US", + "age": "16", + }) + + sk, err := msk.KeyGen(prng, attrs) + test.CheckNoErr(t, err, "master key generation failed") + + policy := cpabe.Policy{} + err = policy.FromString(`(occupation: doctor) and (country: US)`) + test.CheckNoErr(t, err, "policy parsing failed") + + const N = 20 // 2^N bytes of plaintext + buffer := make([]byte, 1<