From 2dcdb295ed051a02956140122b3aa32dc444d76d Mon Sep 17 00:00:00 2001 From: Derek Trider Date: Fri, 1 Sep 2023 13:56:06 -0400 Subject: [PATCH] feat(sdk): Custom masking string option (#586) Also updated docs Signed-off-by: Derek Trider --- .../display/displaydata.go | 4 +- cmd/wallet-sdk-gomobile/display/opts.go | 30 ++++++ cmd/wallet-sdk-gomobile/display/resolve.go | 6 ++ .../display/resolve_test.go | 62 ++++++++++++ cmd/wallet-sdk-gomobile/docs/usage.md | 47 +++++++++ pkg/credentialschema/credentialdisplay.go | 25 ++--- pkg/credentialschema/credentialschema.go | 10 +- pkg/credentialschema/credentialschema_test.go | 99 ++++++++++++++++--- pkg/credentialschema/models.go | 18 ++-- pkg/credentialschema/opts.go | 42 ++++++-- 10 files changed, 300 insertions(+), 43 deletions(-) diff --git a/cmd/wallet-sdk-gomobile/display/displaydata.go b/cmd/wallet-sdk-gomobile/display/displaydata.go index c62d78a6..64892112 100644 --- a/cmd/wallet-sdk-gomobile/display/displaydata.go +++ b/cmd/wallet-sdk-gomobile/display/displaydata.go @@ -215,11 +215,11 @@ func (c *Claim) ValueType() string { // For example, if the UI were to display "Given Name: Alice", then the Value would be "Alice". // If no special formatting was applied to the display value, then this method will be equivalent to calling RawValue. func (c *Claim) Value() string { - if c.claim.Value == "" { + if c.claim.Value == nil { return c.claim.RawValue } - return c.claim.Value + return *c.claim.Value } // RawValue returns the raw display value for this claim without any formatting. diff --git a/cmd/wallet-sdk-gomobile/display/opts.go b/cmd/wallet-sdk-gomobile/display/opts.go index 8964a79c..40b0944f 100644 --- a/cmd/wallet-sdk-gomobile/display/opts.go +++ b/cmd/wallet-sdk-gomobile/display/opts.go @@ -19,6 +19,7 @@ type Opts struct { additionalHeaders api.Headers httpTimeout *time.Duration disableHTTPClientTLSVerification bool + maskingString *string } // NewOpts returns a new Opts object. @@ -69,3 +70,32 @@ func (o *Opts) DisableHTTPClientTLSVerify() *Opts { return o } + +// SetMaskingString sets the string to be used when creating masked values for display. +// The substitution is done on a character-by-character basis, whereby each individual character to be masked +// will be replaced by the entire string. See the examples below to better understand exactly how the +// substitution works. +// +// (Note that any quote characters in the examples below are only there for readability reasons - they're not actually +// part of the values.) +// +// Scenario: The unmasked display value is 12345, and the issuer's metadata specifies that the first 3 characters are +// to be masked. The most common use-case is to substitute every masked character with a single character. This is +// achieved by specifying just a single character in the maskingString. Here's what the masked value would look like +// with different maskingString choices: +// +// maskingString: "•" --> •••45 +// maskingString: "*" --> ***45 +// +// It's also possible to specify multiple characters in the maskingString, or even an empty string if so desired. +// Here's what the masked value would like in such cases: +// +// maskingString: "???" --> ?????????45 +// maskingString: "" --> 45 +// +// If this option isn't used, then by default "•" characters (without the quotes) will be used for masking. +func (o *Opts) SetMaskingString(maskingString string) *Opts { + o.maskingString = &maskingString + + return o +} diff --git a/cmd/wallet-sdk-gomobile/display/resolve.go b/cmd/wallet-sdk-gomobile/display/resolve.go index c4a6523f..bcb61875 100644 --- a/cmd/wallet-sdk-gomobile/display/resolve.go +++ b/cmd/wallet-sdk-gomobile/display/resolve.go @@ -67,6 +67,12 @@ func generateGoAPIOpts(vcs *verifiable.CredentialsArray, issuerURI string, goAPIOpts = append(goAPIOpts, goAPIOpt) } + if opts.maskingString != nil { + goAPIOpt := goapicredentialschema.WithMaskingString(*opts.maskingString) + + goAPIOpts = append(goAPIOpts, goAPIOpt) + } + return goAPIOpts, nil } diff --git a/cmd/wallet-sdk-gomobile/display/resolve_test.go b/cmd/wallet-sdk-gomobile/display/resolve_test.go index 6a4490df..f331e301 100644 --- a/cmd/wallet-sdk-gomobile/display/resolve_test.go +++ b/cmd/wallet-sdk-gomobile/display/resolve_test.go @@ -21,6 +21,11 @@ import ( "github.com/trustbloc/wallet-sdk/cmd/wallet-sdk-gomobile/verifiable" ) +const ( + sensitiveIDLabel = "Sensitive ID" + reallySensitiveIDLabel = "Really Sensitive ID" +) + var ( //go:embed testdata/issuer_metadata.json sampleIssuerMetadata []byte @@ -67,6 +72,62 @@ func TestResolve(t *testing.T) { vcs := verifiable.NewCredentialsArray() vcs.Add(vc) + t.Run("With custom masking string", func(t *testing.T) { + t.Run(`"*"`, func(t *testing.T) { + opts := display.NewOpts().SetMaskingString("*") + + resolvedDisplayData, err := display.Resolve(vcs, server.URL, opts) + require.NoError(t, err) + + credentialDisplay := resolvedDisplayData.CredentialDisplayAtIndex(0) + + for i := 0; i < credentialDisplay.ClaimsLength(); i++ { + claim := credentialDisplay.ClaimAtIndex(i) + + if claim.Label() == sensitiveIDLabel { + require.Equal(t, "*****6789", claim.Value()) + } else if claim.Label() == reallySensitiveIDLabel { + require.Equal(t, "*******", claim.Value()) + } + } + }) + t.Run(`"+++"`, func(t *testing.T) { + opts := display.NewOpts().SetMaskingString("+++") + + resolvedDisplayData, err := display.Resolve(vcs, server.URL, opts) + require.NoError(t, err) + + credentialDisplay := resolvedDisplayData.CredentialDisplayAtIndex(0) + + for i := 0; i < credentialDisplay.ClaimsLength(); i++ { + claim := credentialDisplay.ClaimAtIndex(i) + + if claim.Label() == sensitiveIDLabel { + require.Equal(t, "+++++++++++++++6789", claim.Value()) + } else if claim.Label() == reallySensitiveIDLabel { + require.Equal(t, "+++++++++++++++++++++", claim.Value()) + } + } + }) + t.Run(`"" (empty string`, func(t *testing.T) { + opts := display.NewOpts().SetMaskingString("") + + resolvedDisplayData, err := display.Resolve(vcs, server.URL, opts) + require.NoError(t, err) + + credentialDisplay := resolvedDisplayData.CredentialDisplayAtIndex(0) + + for i := 0; i < credentialDisplay.ClaimsLength(); i++ { + claim := credentialDisplay.ClaimAtIndex(i) + + if claim.Label() == sensitiveIDLabel { + require.Equal(t, "6789", claim.Value()) + } else if claim.Label() == reallySensitiveIDLabel { + require.Equal(t, "", claim.Value()) + } + } + }) + }) t.Run("Without additional headers", func(t *testing.T) { t.Run("Without a preferred locale specified", func(t *testing.T) { opts := display.NewOpts().SetHTTPTimeoutNanoseconds(0) @@ -100,6 +161,7 @@ func TestResolve(t *testing.T) { checkResolvedDisplayData(t, resolvedDisplayData) }) }) + t.Run("No credentials specified", func(t *testing.T) { resolvedDisplayData, err := display.Resolve(nil, "", nil) require.EqualError(t, err, "no credentials specified") diff --git a/cmd/wallet-sdk-gomobile/docs/usage.md b/cmd/wallet-sdk-gomobile/docs/usage.md index d734813a..d9755a1e 100644 --- a/cmd/wallet-sdk-gomobile/docs/usage.md +++ b/cmd/wallet-sdk-gomobile/docs/usage.md @@ -1108,6 +1108,53 @@ from the issuer. See [Resolve Display](#resolve-display) for more information. Display data objects can be serialized using the `serialize()` method (useful for storage) and parsed from serialized form back into display data objects using the `parseDisplayData()` function. +## Options + +The `resolveDisplay` function has a number of different options available. For a full list of available options, check +the associated `Opts` object. This section will highlight some especially notable ones: + +### Set Masking String + +The `setMaskingString(maskingString)` option allows you to specify the string to be used when creating masked values for +display. The substitution is done on a character-by-character basis, whereby each individual character to be masked +will be replaced by the entire string. See the examples below to better understand exactly how the substitution works. + +#### Examples + +Note that any quote characters used in these examples are only there for readability reasons - they're not actually +part of the values. + +Scenario: The unmasked display value is 12345, and the issuer's metadata specifies that the first 3 characters are +to be masked. The most common use-case is to substitute every masked character with a single character. This is +achieved by specifying just a single character in the `maskingString`. Here's what the masked value would look like +with different `maskingString` choices: +``` +maskingString="•" --> •••45 +maskingString="*" --> ***45 +``` + +It's also possible to specify multiple characters in the `maskingString`, or even an empty string if so desired. +Here's what the masked value would like in such cases: + +``` +maskingString="???" --> ?????????45 +maskingString="" --> 45 +``` + +If this option isn't used, then by default "•" characters (without the quotes) will be used for masking. + +### Set Preferred Locale + +Use the `setPreferredLocale` method to specify what locale to use for resolving display values. The effectiveness of +this option is contingent on what information the issuer provides. If the issuer specifies display value localizations +for your preferred locale, then those will be used whenever possible. If localized values aren't available (for your +preferred locale), then the first locale listed by the issuer will be used instead. Note that some pieces of display +data may be localized more or less than other pieces. + +To determine what locales the resolved display values are in, use the various `locale()` methods available in the +display data. You can use this to figure out which display values are actually in your preferred locale and which +used a fallback default locale instead. + ### The Display Object Structure The structure of the display data object is as follows: diff --git a/pkg/credentialschema/credentialdisplay.go b/pkg/credentialschema/credentialdisplay.go index ad7f27c5..b5c823b7 100644 --- a/pkg/credentialschema/credentialdisplay.go +++ b/pkg/credentialschema/credentialdisplay.go @@ -23,7 +23,7 @@ var ( ) func buildCredentialDisplays(vcs []*verifiable.Credential, credentialsSupported []issuer.SupportedCredential, - preferredLocale string, + preferredLocale, maskingString string, ) ([]CredentialDisplay, error) { var credentialDisplays []CredentialDisplay @@ -46,7 +46,8 @@ func buildCredentialDisplays(vcs []*verifiable.Credential, credentialsSupported continue } - credentialDisplay, err := buildCredentialDisplay(&credentialsSupported[i], subject, preferredLocale) + credentialDisplay, err := buildCredentialDisplay(&credentialsSupported[i], subject, preferredLocale, + maskingString) if err != nil { return nil, err } @@ -92,9 +93,9 @@ func haveMatchingTypes(supportedCredential *issuer.SupportedCredential, vc *veri } func buildCredentialDisplay(supportedCredential *issuer.SupportedCredential, subject *verifiable.Subject, - preferredLocale string, + preferredLocale, maskingString string, ) (*CredentialDisplay, error) { - resolvedClaims, err := resolveClaims(supportedCredential, subject, preferredLocale) + resolvedClaims, err := resolveClaims(supportedCredential, subject, preferredLocale, maskingString) if err != nil { return nil, err } @@ -145,14 +146,14 @@ func getSubject(vc *verifiable.Credential) (*verifiable.Subject, error) { } func resolveClaims(supportedCredential *issuer.SupportedCredential, credentialSubject *verifiable.Subject, - preferredLocale string, + preferredLocale, maskingString string, ) ([]ResolvedClaim, error) { var resolvedClaims []ResolvedClaim for fieldName, claim := range supportedCredential.CredentialSubject { claim := claim // Resolves implicit memory aliasing warning from linter - resolvedClaim, err := resolveClaim(fieldName, &claim, credentialSubject, preferredLocale) + resolvedClaim, err := resolveClaim(fieldName, &claim, credentialSubject, preferredLocale, maskingString) if err != nil && !errors.Is(err, errNoClaimDisplays) && !errors.Is(err, errClaimValueNotFoundInVC) { return nil, err } @@ -166,7 +167,7 @@ func resolveClaims(supportedCredential *issuer.SupportedCredential, credentialSu } func resolveClaim(fieldName string, claim *issuer.Claim, credentialSubject *verifiable.Subject, - preferredLocale string, + preferredLocale, maskingString string, ) (*ResolvedClaim, error) { if len(claim.LocalizedClaimDisplays) == 0 { return nil, errNoClaimDisplays @@ -181,15 +182,15 @@ func resolveClaim(fieldName string, claim *issuer.Claim, credentialSubject *veri rawValue := fmt.Sprintf("%v", untypedValue) - var value string + var value *string if claim.Mask != "" { - maskedValue, err := getMaskedValue(rawValue, claim.Mask) + maskedValue, err := getMaskedValue(rawValue, claim.Mask, maskingString) if err != nil { return nil, err } - value = maskedValue + value = &maskedValue } return &ResolvedClaim{ @@ -205,7 +206,7 @@ func resolveClaim(fieldName string, claim *issuer.Claim, credentialSubject *veri }, nil } -func getMaskedValue(rawValue, maskingPattern string) (string, error) { +func getMaskedValue(rawValue, maskingPattern, maskingString string) (string, error) { // Trim "regex(" from the beginning and ")" from the end regex := maskingPattern[6 : len(maskingPattern)-1] @@ -217,7 +218,7 @@ func getMaskedValue(rawValue, maskingPattern string) (string, error) { // Always use the first submatch. valueToBeMasked := r.ReplaceAllString(rawValue, "$1") - maskedValue := strings.ReplaceAll(rawValue, valueToBeMasked, strings.Repeat("•", len(valueToBeMasked))) + maskedValue := strings.ReplaceAll(rawValue, valueToBeMasked, strings.Repeat(maskingString, len(valueToBeMasked))) return maskedValue, nil } diff --git a/pkg/credentialschema/credentialschema.go b/pkg/credentialschema/credentialschema.go index 6b17e9da..20e1a847 100644 --- a/pkg/credentialschema/credentialschema.go +++ b/pkg/credentialschema/credentialschema.go @@ -12,12 +12,18 @@ package credentialschema // same order. // This method requires one VC source and one issuer metadata source. See opts.go for more information. func Resolve(opts ...ResolveOpt) (*ResolvedDisplayData, error) { - vcs, metadata, preferredLocale, err := processOpts(opts) + vcs, metadata, preferredLocale, maskingString, err := processOpts(opts) if err != nil { return nil, err } - credentialDisplays, err := buildCredentialDisplays(vcs, metadata.CredentialsSupported, preferredLocale) + if maskingString == nil { + defaultMaskingString := "•" + maskingString = &defaultMaskingString + } + + credentialDisplays, err := buildCredentialDisplays(vcs, metadata.CredentialsSupported, preferredLocale, + *maskingString) if err != nil { return nil, err } diff --git a/pkg/credentialschema/credentialschema_test.go b/pkg/credentialschema/credentialschema_test.go index 94b9f2e6..84688482 100644 --- a/pkg/credentialschema/credentialschema_test.go +++ b/pkg/credentialschema/credentialschema_test.go @@ -22,6 +22,11 @@ import ( "github.com/trustbloc/wallet-sdk/pkg/models/issuer" ) +const ( + sensitiveIDLabel = "Sensitive ID" + reallySensitiveIDLabel = "Really Sensitive ID" +) + var ( //go:embed testdata/issuer_metadata.json sampleIssuerMetadata []byte @@ -54,7 +59,7 @@ func (m *mockIssuerServerHandler) ServeHTTP(writer http.ResponseWriter, _ *http. } } -func TestResolve(t *testing.T) { +func TestResolve(t *testing.T) { //nolint: gocognit // Test file t.Run("Success", func(t *testing.T) { t.Run("Credentials supported object contains display info for the given VC", func(t *testing.T) { credential, err := verifiable.ParseCredential(credentialUniversityDegree, @@ -129,21 +134,75 @@ func TestResolve(t *testing.T) { }) t.Run("Credentials supported object does not contain display info for the given VC, "+ "resulting in the default display being used", func(t *testing.T) { - var issuerMetadata issuer.Metadata + var metadata issuer.Metadata - err = json.Unmarshal(sampleIssuerMetadata, &issuerMetadata) + err = json.Unmarshal(sampleIssuerMetadata, &metadata) require.NoError(t, err) - issuerMetadata.CredentialsSupported[0].Types[1] = "SomeOtherType" + metadata.CredentialsSupported[0].Types[1] = "SomeOtherType" resolvedDisplayData, errResolve := credentialschema.Resolve( credentialschema.WithCredentials([]*verifiable.Credential{credential}), - credentialschema.WithIssuerMetadata(&issuerMetadata), + credentialschema.WithIssuerMetadata(&metadata), credentialschema.WithHTTPClient(http.DefaultClient), credentialschema.WithPreferredLocale("en-US")) require.NoError(t, errResolve) checkForDefaultDisplayData(t, resolvedDisplayData) }) + t.Run("With custom masking string", func(t *testing.T) { + t.Run(`"*"`, func(t *testing.T) { + resolvedDisplayData, errResolve := credentialschema.Resolve( + credentialschema.WithCredentials([]*verifiable.Credential{credential}), + credentialschema.WithIssuerMetadata(&issuerMetadata), + credentialschema.WithMaskingString("*")) + require.NoError(t, errResolve) + + claims := resolvedDisplayData.CredentialDisplays[0].Claims + + for _, claim := range claims { + if claim.Label == sensitiveIDLabel { + require.Equal(t, "*****6789", *claim.Value) + } else if claim.Label == reallySensitiveIDLabel { + require.Equal(t, "*******", *claim.Value) + } + } + }) + t.Run(`"+++"`, func(t *testing.T) { + resolvedDisplayData, errResolve := credentialschema.Resolve( + credentialschema.WithCredentials([]*verifiable.Credential{credential}), + credentialschema.WithIssuerMetadata(&issuerMetadata), + credentialschema.WithMaskingString("+++")) + require.NoError(t, errResolve) + + claims := resolvedDisplayData.CredentialDisplays[0].Claims + + for _, claim := range claims { + if claim.Label == sensitiveIDLabel { + require.Equal(t, "+++++++++++++++6789", *claim.Value) + } else if claim.Label == reallySensitiveIDLabel { + require.Equal(t, "+++++++++++++++++++++", *claim.Value) + } + } + }) + t.Run(`"" (empty string)`, func(t *testing.T) { + resolvedDisplayData, errResolve := credentialschema.Resolve( + credentialschema.WithCredentials([]*verifiable.Credential{credential}), + credentialschema.WithIssuerMetadata(&issuerMetadata), + credentialschema.WithMaskingString("")) + require.NoError(t, errResolve) + + claims := resolvedDisplayData.CredentialDisplays[0].Claims + + for _, claim := range claims { + if claim.Label == sensitiveIDLabel { + require.Equal(t, "6789", *claim.Value) + } else if claim.Label == reallySensitiveIDLabel { + require.Equal(t, "", *claim.Value) + } + } + }) + }) + t.Run("Credentials supported object does not have claim display info", func(t *testing.T) { var issuerMetadata issuer.Metadata @@ -201,6 +260,8 @@ func TestResolve(t *testing.T) { expectedIDOrder := 0 expectedGivenNameOrder := 1 expectedSurnameOrder := 2 + sensitiveIDValue := "•••••6789" + reallySensitiveIDValue := "•••••••" expectedClaims := []credentialschema.ResolvedClaim{ {RawID: "id", Label: "ID", RawValue: "1234", ValueType: "string", Locale: "en-US", Order: &expectedIDOrder}, { @@ -214,11 +275,11 @@ func TestResolve(t *testing.T) { {RawID: "gpa", Label: "GPA", RawValue: "4.0", ValueType: "number", Locale: "en-US"}, { RawID: "sensitive_id", Label: "Sensitive ID", RawValue: "123456789", - Value: "•••••6789", ValueType: "string", Mask: "regex(^(.*).{4}$)", Locale: "en-US", + Value: &sensitiveIDValue, ValueType: "string", Mask: "regex(^(.*).{4}$)", Locale: "en-US", }, { RawID: "really_sensitive_id", Label: "Really Sensitive ID", RawValue: "abcdefg", - Value: "•••••••", ValueType: "string", Mask: "regex((.*))", Locale: "en-US", + Value: &reallySensitiveIDValue, ValueType: "string", Mask: "regex((.*))", Locale: "en-US", }, { RawID: "chemistry", Label: "Chemistry Final Grade", RawValue: "78", @@ -420,6 +481,8 @@ func checkSuccessCaseMatchedDisplayData(t *testing.T, resolvedDisplayData *crede expectedIDOrder := 0 expectedGivenNameOrder := 1 expectedSurnameOrder := 2 + sensitiveIDValue := "•••••6789" + reallySensitiveIDValue := "•••••••" expectedClaims := []credentialschema.ResolvedClaim{ {RawID: "id", Label: "ID", RawValue: "1234", ValueType: "string", Locale: "en-US", Order: &expectedIDOrder}, { @@ -432,12 +495,12 @@ func checkSuccessCaseMatchedDisplayData(t *testing.T, resolvedDisplayData *crede }, {RawID: "gpa", Label: "GPA", RawValue: "4.0", ValueType: "number", Locale: "en-US"}, { - RawID: "sensitive_id", Label: "Sensitive ID", RawValue: "123456789", Value: "•••••6789", + RawID: "sensitive_id", Label: "Sensitive ID", RawValue: "123456789", Value: &sensitiveIDValue, ValueType: "string", Mask: "regex(^(.*).{4}$)", Locale: "en-US", }, { - RawID: "really_sensitive_id", Label: "Really Sensitive ID", RawValue: "abcdefg", Value: "•••••••", - ValueType: "string", Mask: "regex((.*))", Locale: "en-US", + RawID: "really_sensitive_id", Label: "Really Sensitive ID", RawValue: "abcdefg", + Value: &reallySensitiveIDValue, ValueType: "string", Mask: "regex((.*))", Locale: "en-US", }, { RawID: "chemistry", Label: "Chemistry Final Grade", RawValue: "78", @@ -535,9 +598,21 @@ func verifyClaimsAnyOrder(t *testing.T, actualClaims []credentialschema.Resolved } } -func claimsMatch(claim1, claim2 *credentialschema.ResolvedClaim) bool { +func claimsMatch(claim1, claim2 *credentialschema.ResolvedClaim) bool { //nolint: gocyclo // Test file + // Check the value pointer fields first. + if claim1.Value != nil { + if claim2.Value == nil { + return false + } + + if *claim1.Value != *claim2.Value { + return false + } + } else if claim2.Value != nil { + return false + } + if claim1.Label == claim2.Label && - claim1.Value == claim2.Value && claim1.Locale == claim2.Locale && claim1.ValueType == claim2.ValueType && claim1.RawID == claim2.RawID && diff --git a/pkg/credentialschema/models.go b/pkg/credentialschema/models.go index f1077ed2..87ee6263 100644 --- a/pkg/credentialschema/models.go +++ b/pkg/credentialschema/models.go @@ -40,15 +40,15 @@ type CredentialOverview struct { type ResolvedClaim struct { // RawID is the raw field name (key) from the VC associated with this claim. // It's not localized or formatted for display. - RawID string `json:"raw_id,omitempty"` - Label string `json:"label,omitempty"` - ValueType string `json:"value_type,omitempty"` - RawValue string `json:"raw_value,omitempty"` - Value string `json:"value,omitempty"` - Order *int `json:"order,omitempty"` - Pattern string `json:"pattern,omitempty"` - Mask string `json:"mask,omitempty"` - Locale string `json:"locale,omitempty"` + RawID string `json:"raw_id,omitempty"` + Label string `json:"label,omitempty"` + ValueType string `json:"value_type,omitempty"` + RawValue string `json:"raw_value,omitempty"` + Value *string `json:"value,omitempty"` + Order *int `json:"order,omitempty"` + Pattern string `json:"pattern,omitempty"` + Mask string `json:"mask,omitempty"` + Locale string `json:"locale,omitempty"` } // Logo represents display information for a logo. diff --git a/pkg/credentialschema/opts.go b/pkg/credentialschema/opts.go index a365cd70..dab3c60e 100644 --- a/pkg/credentialschema/opts.go +++ b/pkg/credentialschema/opts.go @@ -51,6 +51,7 @@ type resolveOpts struct { preferredLocal string metricsLogger api.MetricsLogger httpClient httpClient + maskingString *string } // ResolveOpt represents an option for the Resolve function. @@ -120,12 +121,41 @@ func WithHTTPClient(httpClient httpClient) ResolveOpt { } } -func processOpts(opts []ResolveOpt) ([]*verifiable.Credential, *issuer.Metadata, string, error) { +// WithMaskingString is an option allowing a caller to specify a string to be used when creating masked values for +// display. The substitution is done on a character-by-character basis, whereby each individual character to be masked +// will be replaced by the entire string. See the examples below to better understand exactly how the +// substitution works. +// +// (Note that any quote characters in the examples below are only there for readability reasons - they're not actually +// part of the values.) +// +// Scenario: The unmasked display value is 12345, and the issuer's metadata specifies that the first 3 characters are +// to be masked. The most common use-case is to substitute every masked character with a single character. This is +// achieved by specifying just a single character in the maskingString. Here's what the masked value would look like +// with different maskingString choices: +// +// maskingString: "•" --> •••45 +// maskingString: "*" --> ***45 +// +// It's also possible to specify multiple characters in the maskingString, or even an empty string if so desired. +// Here's what the masked value would like in such cases: +// +// maskingString: "???" --> ?????????45 +// maskingString: "" --> 45 +// +// If this option isn't used, then by default "•" characters (without the quotes) will be used for masking. +func WithMaskingString(maskingString string) ResolveOpt { + return func(opts *resolveOpts) { + opts.maskingString = &maskingString + } +} + +func processOpts(opts []ResolveOpt) ([]*verifiable.Credential, *issuer.Metadata, string, *string, error) { mergedOpts := mergeOpts(opts) err := validateOpts(mergedOpts) if err != nil { - return nil, nil, "", err + return nil, nil, "", nil, err } return processValidatedOpts(mergedOpts) @@ -174,10 +204,10 @@ func validateIssuerMetadataOpts(issuerMetadataSource *issuerMetadataSource) erro return nil } -func processValidatedOpts(opts *resolveOpts) ([]*verifiable.Credential, *issuer.Metadata, string, error) { +func processValidatedOpts(opts *resolveOpts) ([]*verifiable.Credential, *issuer.Metadata, string, *string, error) { vcs, err := processVCOpts(&opts.credentialSource) if err != nil { - return nil, nil, "", err + return nil, nil, "", nil, err } var metricsLogger api.MetricsLogger @@ -194,10 +224,10 @@ func processValidatedOpts(opts *resolveOpts) ([]*verifiable.Credential, *issuer. issuerMetadata, err := processIssuerMetadataOpts(&opts.issuerMetadataSource, opts.httpClient, metricsLogger) if err != nil { - return nil, nil, "", err + return nil, nil, "", nil, err } - return vcs, issuerMetadata, opts.preferredLocal, nil + return vcs, issuerMetadata, opts.preferredLocal, opts.maskingString, nil } func processVCOpts(credentialSource *credentialSource) ([]*verifiable.Credential, error) {