Skip to content

Commit e5c56db

Browse files
feat(sdk): add nanotdf plaintext policy (#2182)
### Proposed Changes * Add nanotdf plaintext policy support * Default to plaintext policy to align with Base TDF ### Checklist - [ ] I have added or updated unit tests - [ ] I have added or updated integration tests (if appropriate) - [ ] I have added or updated documentation ### Testing Instructions --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent 7492757 commit e5c56db

File tree

11 files changed

+291
-107
lines changed

11 files changed

+291
-107
lines changed

examples/cmd/encrypt.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ var (
2323
dataAttributes []string
2424
collection int
2525
alg string
26+
policyMode string
2627
)
2728

2829
func init() {
@@ -40,6 +41,7 @@ func init() {
4041
encryptCmd.Flags().StringVarP(&outputName, "output", "o", "sensitive.txt.tdf", "name or path of output file; - for stdout")
4142
encryptCmd.Flags().StringVarP(&alg, "key-encapsulation-algorithm", "A", "rsa:2048", "Key wrap algorithm algorithm:parameters")
4243
encryptCmd.Flags().IntVarP(&collection, "collection", "c", 0, "number of nano's to create for collection. If collection >0 (default) then output will be <iteration>_<output>")
44+
encryptCmd.Flags().StringVar(&policyMode, "policy-mode", "", "Store policy as encrypted instead of plaintext (nanoTDF only) [plaintext|encrypted]")
4345

4446
ExamplesCmd.AddCommand(&encryptCmd)
4547
}
@@ -146,6 +148,21 @@ func encrypt(cmd *cobra.Command, args []string) error {
146148
if err != nil {
147149
return err
148150
}
151+
152+
// Handle policy mode if nanoTDF
153+
switch policyMode {
154+
case "": // default to encrypted
155+
case "encrypted":
156+
err = nanoTDFConfig.SetPolicyMode(sdk.NanoTDFPolicyModeEncrypted)
157+
case "plaintext":
158+
err = nanoTDFConfig.SetPolicyMode(sdk.NanoTDFPolicyModePlainText)
159+
default:
160+
err = fmt.Errorf("unsupported policy mode: %s", policyMode)
161+
}
162+
if err != nil {
163+
return err
164+
}
165+
149166
for i, writer := range writer {
150167
input := plainText
151168
if collection > 0 {

sdk/granter_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -299,7 +299,7 @@ func TestAttributeFromMalformedURL(t *testing.T) {
299299
t.Run(tc.n, func(t *testing.T) {
300300
a, err := NewAttributeNameFQN(tc.u)
301301
require.ErrorIs(t, err, ErrInvalid)
302-
assert.Equal(t, "", a.String())
302+
assert.Empty(t, a.String())
303303
})
304304
}
305305
}
@@ -342,7 +342,7 @@ func TestAttributeValueFromMalformedURL(t *testing.T) {
342342
t.Run(tc.n, func(t *testing.T) {
343343
a, err := NewAttributeValueFQN(tc.u)
344344
require.ErrorIs(t, err, ErrInvalid)
345-
assert.Equal(t, "", a.String())
345+
assert.Empty(t, a.String())
346346
})
347347
}
348348
}

sdk/nanotdf.go

Lines changed: 65 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,8 @@ type NanoTDFHeader struct {
6161
bindCfg bindingConfig
6262
sigCfg signatureConfig
6363
EphemeralKey []byte
64-
EncryptedPolicyBody []byte
64+
PolicyMode PolicyType
65+
PolicyBody []byte
6566
gmacPolicyBinding []byte
6667
ecdsaPolicyBindingR []byte
6768
ecdsaPolicyBindingS []byte
@@ -90,7 +91,7 @@ func (header *NanoTDFHeader) VerifyPolicyBinding() (bool, error) {
9091
return false, err
9192
}
9293

93-
digest := ocrypto.CalculateSHA256(header.EncryptedPolicyBody)
94+
digest := ocrypto.CalculateSHA256(header.PolicyBody)
9495
if header.IsEcdsaBindingEnabled() {
9596
ephemeralECDSAPublicKey, err := ocrypto.UncompressECPubKey(curve, header.EphemeralKey)
9697
if err != nil {
@@ -499,14 +500,15 @@ func writeNanoTDFHeader(writer io.Writer, config NanoTDFConfig) ([]byte, uint32,
499500
}
500501
totalBytes += uint32(l)
501502

502-
// Write policy - (Policy Mode, Policy length, Policy cipherText, Policy binding)
503-
config.policy.body.mode = policyTypeEmbeddedPolicyEncrypted
503+
// Write policy mode
504+
config.policy.body.mode = config.policyMode
504505
l, err = writer.Write([]byte{byte(config.policy.body.mode)})
505506
if err != nil {
506507
return nil, 0, 0, err
507508
}
508509
totalBytes += uint32(l)
509510

511+
// Create policy object
510512
policyObj, err := createPolicyObject(config.attributes)
511513
if err != nil {
512514
return nil, 0, 0, fmt.Errorf("fail to create policy object:%w", err)
@@ -517,59 +519,34 @@ func writeNanoTDFHeader(writer io.Writer, config NanoTDFConfig) ([]byte, uint32,
517519
return nil, 0, 0, fmt.Errorf("json.Marshal failed:%w", err)
518520
}
519521

520-
ecdhKey, err := ocrypto.ConvertToECDHPrivateKey(config.keyPair.PrivateKey)
522+
// Create the symmetric key
523+
symmetricKey, err := createNanoTDFSymmetricKey(config)
521524
if err != nil {
522-
return nil, 0, 0, fmt.Errorf("ocrypto.ConvertToECDHPrivateKey failed:%w", err)
523-
}
524-
525-
symKey, err := ocrypto.ComputeECDHKeyFromECDHKeys(config.kasPublicKey, ecdhKey)
526-
if err != nil {
527-
return nil, 0, 0, fmt.Errorf("ocrypto.ComputeECDHKeyFromEC failed:%w", err)
528-
}
529-
530-
salt := versionSalt()
531-
532-
symmetricKey, err := ocrypto.CalculateHKDF(salt, symKey)
533-
if err != nil {
534-
return nil, 0, 0, fmt.Errorf("ocrypto.CalculateHKDF failed:%w", err)
535-
}
536-
537-
aesGcm, err := ocrypto.NewAESGcm(symmetricKey)
538-
if err != nil {
539-
return nil, 0, 0, fmt.Errorf("ocrypto.NewAESGcm failed:%w", err)
525+
return nil, 0, 0, err
540526
}
541527

542-
tagSize, err := SizeOfAuthTagForCipher(config.sigCfg.cipher)
543-
if err != nil {
544-
return nil, 0, 0, fmt.Errorf("SizeOfAuthTagForCipher failed:%w", err)
528+
// Set the symmetric key in the collection config
529+
if config.collectionCfg.useCollection {
530+
config.collectionCfg.symKey = symmetricKey
545531
}
546532

547-
const (
548-
kIvLength = 12
549-
)
550-
iv := make([]byte, kIvLength)
551-
cipherText, err := aesGcm.EncryptWithIVAndTagSize(iv, policyObjectAsStr, tagSize)
533+
embeddedP, err := createNanoTDFEmbeddedPolicy(symmetricKey, policyObjectAsStr, config)
552534
if err != nil {
553-
return nil, 0, 0, fmt.Errorf("AesGcm.EncryptWithIVAndTagSize failed:%w", err)
535+
return nil, 0, 0, fmt.Errorf("failed to create embedded policy:%w", err)
554536
}
555537

556-
embeddedP := embeddedPolicy{
557-
lengthBody: uint16(len(cipherText) - len(iv)),
558-
body: cipherText[len(iv):],
559-
}
560538
err = embeddedP.writeEmbeddedPolicy(writer)
561539
if err != nil {
562540
return nil, 0, 0, fmt.Errorf("writeEmbeddedPolicy failed:%w", err)
563541
}
564542

565543
// size of uint16
566-
const (
567-
kSizeOfUint16 = 2
568-
)
544+
const kSizeOfUint16 = 2
569545
totalBytes += kSizeOfUint16 + uint32(len(embeddedP.body))
570546

571547
digest := ocrypto.CalculateSHA256(embeddedP.body)
572-
if config.bindCfg.useEcdsaBinding { //nolint:nestif // todo: subfunction
548+
549+
if config.bindCfg.useEcdsaBinding { //nolint:nestif // TODO: refactor
573550
rBytes, sBytes, err := ocrypto.ComputeECDSASig(digest, config.keyPair.PrivateKey)
574551
if err != nil {
575552
return nil, 0, 0, fmt.Errorf("ComputeECDSASig failed:%w", err)
@@ -617,19 +594,15 @@ func writeNanoTDFHeader(writer io.Writer, config NanoTDFConfig) ([]byte, uint32,
617594
}
618595
totalBytes += uint32(l)
619596

620-
if config.collectionCfg.useCollection {
621-
config.collectionCfg.symKey = symmetricKey
622-
}
623-
624597
return symmetricKey, totalBytes, 0, nil
625598
}
626599

627600
func NewNanoTDFHeaderFromReader(reader io.Reader) (NanoTDFHeader, uint32, error) {
628601
header := NanoTDFHeader{}
629602
var size uint32
630603

604+
// Read and validate magic number
631605
magicNumber := make([]byte, len(kNanoTDFMagicStringAndVersion))
632-
633606
l, err := reader.Read(magicNumber)
634607
if err != nil {
635608
return header, 0, fmt.Errorf(" io.Reader.Read failed :%w", err)
@@ -643,7 +616,7 @@ func NewNanoTDFHeaderFromReader(reader io.Reader) (NanoTDFHeader, uint32, error)
643616
return header, 0, errors.New("not a valid nano tdf")
644617
}
645618

646-
// read resource locator
619+
// Read resource locator
647620
resource, err := NewResourceLocatorFromReader(reader)
648621
if err != nil {
649622
return header, 0, fmt.Errorf("call to NewResourceLocatorFromReader failed :%w", err)
@@ -653,7 +626,7 @@ func NewNanoTDFHeaderFromReader(reader io.Reader) (NanoTDFHeader, uint32, error)
653626

654627
slog.Debug("NewNanoTDFHeaderFromReader", slog.Uint64("resource locator", uint64(resource.getLength())))
655628

656-
// read ECC and Binding Mode
629+
// Read ECC and Binding Mode
657630
oneBytes := make([]byte, 1)
658631
l, err = reader.Read(oneBytes)
659632
if err != nil {
@@ -662,12 +635,12 @@ func NewNanoTDFHeaderFromReader(reader io.Reader) (NanoTDFHeader, uint32, error)
662635
size += uint32(l)
663636
header.bindCfg = deserializeBindingCfg(oneBytes[0])
664637

665-
// check ephemeral ECC Params Enum
638+
// Check ephemeral ECC Params Enum
666639
if header.bindCfg.eccMode != ocrypto.ECCModeSecp256r1 {
667640
return header, 0, errors.New("current implementation of nano tdf only support secp256r1 curve")
668641
}
669642

670-
// read Payload and Sig Mode
643+
// Read Payload and Sig Mode
671644
l, err = reader.Read(oneBytes)
672645
if err != nil {
673646
return header, 0, fmt.Errorf(" io.Reader.Read failed :%w", err)
@@ -682,14 +655,13 @@ func NewNanoTDFHeaderFromReader(reader io.Reader) (NanoTDFHeader, uint32, error)
682655
}
683656
size += uint32(l)
684657

685-
if oneBytes[0] != uint8(policyTypeEmbeddedPolicyEncrypted) {
686-
return header, 0, errors.New(" current implementation only support embedded policy type")
658+
policyMode := PolicyType(oneBytes[0])
659+
if err := validNanoTDFPolicyMode(policyMode); err != nil {
660+
return header, 0, errors.Join(fmt.Errorf("unsupported policy mode: %v", policyMode), err)
687661
}
688662

689-
// read policy length
690-
const (
691-
kSizeOfUint16 = 2
692-
)
663+
// Read policy length
664+
const kSizeOfUint16 = 2
693665
twoBytes := make([]byte, kSizeOfUint16)
694666
l, err = reader.Read(twoBytes)
695667
if err != nil {
@@ -699,17 +671,18 @@ func NewNanoTDFHeaderFromReader(reader io.Reader) (NanoTDFHeader, uint32, error)
699671
policyLength := binary.BigEndian.Uint16(twoBytes)
700672
slog.Debug("NewNanoTDFHeaderFromReader", slog.Uint64("policyLength", uint64(policyLength)))
701673

702-
// read policy body
703-
header.EncryptedPolicyBody = make([]byte, policyLength)
704-
l, err = reader.Read(header.EncryptedPolicyBody)
674+
// Read policy body
675+
header.PolicyMode = policyMode
676+
header.PolicyBody = make([]byte, policyLength)
677+
l, err = reader.Read(header.PolicyBody)
705678
if err != nil {
706679
return header, 0, fmt.Errorf(" io.Reader.Read failed :%w", err)
707680
}
708681
size += uint32(l)
709682

710-
// read policy binding
711-
if header.bindCfg.useEcdsaBinding { //nolint:nestif // todo: subfunction
712-
// read rBytes len and its contents
683+
// Read policy binding
684+
if header.bindCfg.useEcdsaBinding { //nolint:nestif // TODO: refactor
685+
// Read rBytes len and its contents
713686
l, err = reader.Read(oneBytes)
714687
if err != nil {
715688
return header, 0, fmt.Errorf(" io.Reader.Read failed :%w", err)
@@ -723,7 +696,7 @@ func NewNanoTDFHeaderFromReader(reader io.Reader) (NanoTDFHeader, uint32, error)
723696
}
724697
size += uint32(l)
725698

726-
// read sBytes len and its contents
699+
// Read sBytes len and its contents
727700
l, err = reader.Read(oneBytes)
728701
if err != nil {
729702
return header, 0, fmt.Errorf(" io.Reader.Read failed :%w", err)
@@ -750,7 +723,7 @@ func NewNanoTDFHeaderFromReader(reader io.Reader) (NanoTDFHeader, uint32, error)
750723
return header, 0, fmt.Errorf("getECCKeyLength :%w", err)
751724
}
752725

753-
// read ephemeral Key
726+
// Read ephemeral Key
754727
ephemeralKey := make([]byte, ephemeralKeySize)
755728
l, err = reader.Read(ephemeralKey)
756729
if err != nil {
@@ -995,8 +968,8 @@ func (n *NanoTDFDecryptHandler) Decrypt(_ context.Context, result []kaoResult) (
995968
payloadLength := binary.BigEndian.Uint32(payloadLengthBuf)
996969
slog.Debug("ReadNanoTDF", slog.Uint64("payloadLength", uint64(payloadLength)))
997970

998-
cipherDate := make([]byte, payloadLength)
999-
_, err = n.reader.Read(cipherDate)
971+
cipherData := make([]byte, payloadLength)
972+
_, err = n.reader.Read(cipherData)
1000973
if err != nil {
1001974
return 0, fmt.Errorf("readSeeker.Seek failed: %w", err)
1002975
}
@@ -1009,15 +982,15 @@ func (n *NanoTDFDecryptHandler) Decrypt(_ context.Context, result []kaoResult) (
1009982
ivPadded := make([]byte, 0, ocrypto.GcmStandardNonceSize)
1010983
noncePadding := make([]byte, kIvPadding)
1011984
ivPadded = append(ivPadded, noncePadding...)
1012-
iv := cipherDate[:kNanoTDFIvSize]
985+
iv := cipherData[:kNanoTDFIvSize]
1013986
ivPadded = append(ivPadded, iv...)
1014987

1015988
tagSize, err := SizeOfAuthTagForCipher(n.header.sigCfg.cipher)
1016989
if err != nil {
1017990
return 0, fmt.Errorf("SizeOfAuthTagForCipher failed:%w", err)
1018991
}
1019992

1020-
decryptedData, err := aesGcm.DecryptWithIVAndTagSize(ivPadded, cipherDate[kNanoTDFIvSize:], tagSize)
993+
decryptedData, err := aesGcm.DecryptWithIVAndTagSize(ivPadded, cipherData[kNanoTDFIvSize:], tagSize)
1021994
if err != nil {
1022995
return 0, err
1023996
}
@@ -1108,3 +1081,28 @@ func versionSalt() []byte {
11081081
digest.Write([]byte(kNanoTDFMagicStringAndVersion))
11091082
return digest.Sum(nil)
11101083
}
1084+
1085+
// createNanoTDFSymmetricKey creates the symmetric key for nanoTDF header
1086+
func createNanoTDFSymmetricKey(config NanoTDFConfig) ([]byte, error) {
1087+
if config.kasPublicKey == nil {
1088+
return nil, fmt.Errorf("KAS public key is required for encrypted policy mode")
1089+
}
1090+
1091+
ecdhKey, err := ocrypto.ConvertToECDHPrivateKey(config.keyPair.PrivateKey)
1092+
if err != nil {
1093+
return nil, fmt.Errorf("ocrypto.ConvertToECDHPrivateKey failed:%w", err)
1094+
}
1095+
1096+
symKey, err := ocrypto.ComputeECDHKeyFromECDHKeys(config.kasPublicKey, ecdhKey)
1097+
if err != nil {
1098+
return nil, fmt.Errorf("ocrypto.ComputeECDHKeyFromEC failed:%w", err)
1099+
}
1100+
1101+
salt := versionSalt()
1102+
symmetricKey, err := ocrypto.CalculateHKDF(salt, symKey)
1103+
if err != nil {
1104+
return nil, fmt.Errorf("ocrypto.CalculateHKDF failed:%w", err)
1105+
}
1106+
1107+
return symmetricKey, nil
1108+
}

sdk/nanotdf_config.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ type NanoTDFConfig struct {
2626
policy policyInfo
2727
bindCfg bindingConfig
2828
collectionCfg *collectionConfig
29+
policyMode PolicyType // Added field for policy mode
2930
}
3031

3132
type NanoTDFOption func(*NanoTDFConfig) error
@@ -55,6 +56,7 @@ func (s SDK) NewNanoTDFConfig() (*NanoTDFConfig, error) {
5556
useCollection: false,
5657
header: []byte{},
5758
},
59+
policyMode: NanoTDFPolicyModeDefault,
5860
}
5961

6062
return c, nil
@@ -89,6 +91,15 @@ func (config *NanoTDFConfig) EnableCollection() {
8991
config.collectionCfg.useCollection = true
9092
}
9193

94+
// SetPolicyMode sets whether the policy should be encrypted or plaintext
95+
func (config *NanoTDFConfig) SetPolicyMode(mode PolicyType) error {
96+
if err := validNanoTDFPolicyMode(mode); err != nil {
97+
return err
98+
}
99+
config.policyMode = mode
100+
return nil
101+
}
102+
92103
// WithNanoDataAttributes appends the given data attributes to the bound policy
93104
func WithNanoDataAttributes(attributes ...string) NanoTDFOption {
94105
return func(c *NanoTDFConfig) error {

0 commit comments

Comments
 (0)