diff --git a/README.md b/README.md index 5ceed37..629a372 100644 --- a/README.md +++ b/README.md @@ -274,8 +274,16 @@ EOF ``` Credentials can be generated from the command line with `age-plugin-fido2prf --generate RPID`. Note that they will be usable inside the browser only if the -relying party ID matches the website's origin. +-generate RPID`. The plugin uses USB when available and falls back to PC/SC, +detecting contactless cards as `nfc` and contact cards as `smart-card`. Pass +`-transport usb`, `-transport nfc`, or `-transport smart-card` to override +automatic selection; `nfc` and `smart-card` select only readers with the +matching physical interface. Note that credentials will be usable inside the +browser only if the relying party ID matches the website's origin. + +If multiple matching PC/SC readers contain FIDO2 cards, select one by its exact +name with `-reader`. Reader names are used only during credential generation and +are not stored in the identity. All the features of the plugin are also available as a Go library at [filippo.io/typage/fido2prf](https://pkg.go.dev/filippo.io/typage/fido2prf). diff --git a/fido2prf/cmd/age-plugin-fido2prf/main.go b/fido2prf/cmd/age-plugin-fido2prf/main.go index 52b29c0..e9c1b12 100644 --- a/fido2prf/cmd/age-plugin-fido2prf/main.go +++ b/fido2prf/cmd/age-plugin-fido2prf/main.go @@ -22,10 +22,20 @@ func main() { } generate := flag.String("generate", "", "Generate a new credential for the given relying party ID.") + transport := flag.String("transport", "auto", "Transport to use when generating a credential: auto, usb, nfc, or smart-card.") + reader := flag.String("reader", "", "PC/SC reader name to use when generating a credential.") p.RegisterFlags(nil) flag.Parse() if *generate != "" { + if *transport != "auto" && *transport != "usb" && *transport != "nfc" && *transport != "smart-card" { + fmt.Printf("Error: unsupported transport %q\n", *transport) + os.Exit(1) + } + if *reader != "" && *transport == "usb" { + fmt.Println("Error: -reader can't be used with -transport usb") + os.Exit(1) + } fmt.Fprintf(os.Stderr, "Enter the security key PIN: ") pin, err := term.ReadPassword(int(os.Stdin.Fd())) if err != nil { @@ -34,7 +44,7 @@ func main() { } fmt.Fprintf(os.Stderr, "\r\033[K") // Clear the line. - identity, err := fido2prf.NewCredential(*generate, string(pin)) + identity, err := fido2prf.NewCredentialOnReader(*generate, string(pin), *reader, *transport) if err != nil { fmt.Printf("Error: %s\n", err) os.Exit(1) diff --git a/fido2prf/fido2prf.go b/fido2prf/fido2prf.go index 1ae0d1f..b5aea3b 100644 --- a/fido2prf/fido2prf.go +++ b/fido2prf/fido2prf.go @@ -1,61 +1,202 @@ package fido2prf import ( - "bytes" + "context" "crypto/rand" "crypto/sha256" "encoding/base64" "errors" + "fmt" "filippo.io/age" "filippo.io/age/plugin" "filippo.io/typage/fido2prf/internal/ctap2cbor" - "github.com/keys-pub/go-libfido2" + "github.com/telesma-app/ctap/authenticator" + directhid "github.com/telesma-app/ctap/backend/hid" + directpcsc "github.com/telesma-app/ctap/backend/pcsc" + "github.com/telesma-app/ctap/cose" + "github.com/telesma-app/ctap/credential" + "github.com/telesma-app/ctap/protocol" + ctaptransport "github.com/telesma-app/ctap/transport" + "github.com/telesma-app/ctap/webauthn" + nativepcsc "github.com/telesma-app/pcsc" "golang.org/x/crypto/chacha20poly1305" "golang.org/x/crypto/hkdf" ) -func NewCredential(rpID, pin string) (string, error) { - locs, err := libfido2.DeviceLocations() - if err != nil { - return "", err +type deviceLocation struct { + path string + transport string +} + +func (l deviceLocation) open(ctx context.Context) (ctaptransport.Device, error) { + if l.transport == "usb" { + return directhid.Open(ctx, l.path) } - if len(locs) == 0 { - return "", errors.New("no FIDO2 devices found") + return directpcsc.Open(ctx, l.path) +} + +func deviceLocations(ctx context.Context, transports []string, readerName string) ([]deviceLocation, error) { + auto := len(transports) == 0 + usb := auto && readerName == "" + var pcscTransport string + for _, transport := range transports { + switch transport { + case "auto": + auto = true + usb = readerName == "" + case "usb": + usb = true + case "nfc", "smart-card": + pcscTransport = transport + } } - if len(locs) != 1 { - return "", errors.New("multiple FIDO2 devices found, please remove all but one") + + var locations []deviceLocation + if usb { + for info, err := range directhid.Devices(ctx) { + if err != nil { + return nil, err + } + locations = append(locations, deviceLocation{path: info.Path, transport: "usb"}) + } + } + if auto && len(locations) > 0 { + return locations, nil } - device, err := libfido2.NewDevice(locs[0].Path) + if auto { + pcscTransport = "smart-card" + } + if pcscTransport != "" { + for reader, err := range directpcsc.Devices(ctx) { + if err != nil { + return nil, err + } + if readerName != "" && reader.Name != readerName { + continue + } + card, err := nativepcsc.Open(reader.Name) + if err != nil { + continue + } + cardInterface := card.Interface() + card.Close() + + transport := pcscTransport + switch { + case auto && cardInterface == nativepcsc.CardInterfaceContactless: + transport = "nfc" + case auto && cardInterface == nativepcsc.CardInterfaceContact: + transport = "smart-card" + case auto: + continue + case transport == "nfc" && cardInterface != nativepcsc.CardInterfaceContactless: + continue + case transport == "smart-card" && cardInterface != nativepcsc.CardInterfaceContact: + continue + } + locations = append(locations, deviceLocation{path: reader.Name, transport: transport}) + } + } + return locations, nil +} + +func NewCredential(rpID, pin string, transports ...string) (string, error) { + return NewCredentialOnReader(rpID, pin, "", transports...) +} + +func NewCredentialOnReader(rpID, pin, reader string, transports ...string) (string, error) { + ctx := context.Background() + locations, err := deviceLocations(ctx, transports, reader) if err != nil { return "", err } - a, err := device.MakeCredential( - // The client data hash is not useful without attestation. - bytes.Repeat([]byte{0}, 32), - libfido2.RelyingParty{ID: rpID}, - libfido2.User{ - // These are not used for non-resident credentials, - // but the Go wrapper requires them. + + var device *authenticator.Device + var selectedTransport string + var selectedPath string + for _, location := range locations { + transport, err := location.open(ctx) + if err != nil { + continue + } + candidate, err := authenticator.New(ctx, transport) + if err != nil { + transport.Close() + continue + } + if device != nil { + candidate.Close() + device.Close() + if selectedTransport != "usb" && location.transport != "usb" { + return "", fmt.Errorf( + "multiple FIDO2 devices found in PC/SC readers %q and %q; specify one with -reader", + selectedPath, + location.path, + ) + } + return "", errors.New("multiple FIDO2 devices found, please remove all but one") + } + device = candidate + selectedTransport = location.transport + selectedPath = location.path + } + if device == nil { + return "", errors.New("no FIDO2 devices found") + } + defer device.Close() + + var pinUvAuthToken []byte + options := map[protocol.Option]bool{protocol.OptionResidentKeys: false} + if pin == "" { + options[protocol.OptionUserVerification] = true + } else { + pinUvAuthToken, err = device.GetPinUvAuthTokenUsingPIN( + ctx, + pin, + protocol.PermissionMakeCredential, + rpID, + ) + if err != nil { + return "", err + } + defer clear(pinUvAuthToken) + } + + result, err := device.MakeCredential( + ctx, + pinUvAuthToken, + nil, + credential.PublicKeyCredentialRpEntity{ID: rpID}, + credential.PublicKeyCredentialUserEntity{ + // These are not used for non-resident credentials, but CTAP requires them. ID: []byte{0}, - Name: "age-encryption.org/fido2prf", + Name: label, }, - libfido2.ES256, - pin, - &libfido2.MakeCredentialOpts{ - Extensions: []libfido2.Extension{libfido2.HMACSecretExtension}, - RK: libfido2.False, - UV: libfido2.True, - }) + []credential.PublicKeyCredentialParameters{{ + Type: credential.PublicKeyCredentialTypePublicKey, + Algorithm: cose.AlgorithmES256, + }}, + nil, + &webauthn.CreateAuthenticationExtensionsClientInputs{ + CreateHMACSecretInputs: &webauthn.CreateHMACSecretInputs{ + HMACCreateSecret: true, + }, + }, + options, + 0, + nil, + ) if err != nil { return "", err } - var identity []byte - identity = ctap2cbor.AppendUint(identity, 1) - identity = ctap2cbor.AppendBytes(identity, a.CredentialID) - identity = ctap2cbor.AppendString(identity, rpID) - identity = ctap2cbor.AppendArray(identity, "usb") - return plugin.EncodeIdentity("fido2prf", identity), nil + + var identityData []byte + identityData = ctap2cbor.AppendUint(identityData, 1) + identityData = ctap2cbor.AppendBytes(identityData, result.AuthData.AttestedCredentialData.CredentialID) + identityData = ctap2cbor.AppendString(identityData, rpID) + identityData = ctap2cbor.AppendArray(identityData, selectedTransport) + return plugin.EncodeIdentity("fido2prf", identityData), nil } type Identity struct { @@ -69,75 +210,115 @@ type Identity struct { const label = "age-encryption.org/fido2prf" func (i *Identity) assert(nonce []byte) ([]byte, error) { - locs, err := libfido2.DeviceLocations() + ctx := context.Background() + locations, err := deviceLocations(ctx, i.transports, "") if err != nil { return nil, err } - if len(locs) == 0 { - return nil, errors.New("no FIDO2 devices found") - } - for _, loc := range locs { - device, err := libfido2.NewDevice(loc.Path) + deviceFound := false + for _, location := range locations { + transport, err := location.open(ctx) if err != nil { - return nil, err + continue + } + device, err := authenticator.New(ctx, transport) + if err != nil { + transport.Close() + continue } + deviceFound = true - // First probe to check if the credential ID matches the device, - // before requiring user interaction. - if _, err := device.Assertion( + credentialDescriptor := credential.PublicKeyCredentialDescriptor{ + Type: credential.PublicKeyCredentialTypePublicKey, + ID: i.credentialID, + } + + // First probe to check if the credential ID matches the device, before + // requiring user interaction. + var assertion protocol.AuthenticatorGetAssertionResponse + for assertion, err = range device.GetAssertion( + ctx, + nil, i.relyingParty, - make([]byte, 32), - [][]byte{i.credentialID}, - "", - &libfido2.AssertionOpts{ - UP: libfido2.False, - }, - ); errors.Is(err, libfido2.ErrNoCredentials) { - continue - } else if err != nil { + nil, + []credential.PublicKeyCredentialDescriptor{credentialDescriptor}, + nil, + map[protocol.Option]bool{protocol.OptionUserPresence: false}, + ) { + break + } + if err != nil { + var ctapErr *ctaptransport.CTAPError + if errors.As(err, &ctapErr) && ctapErr.StatusCode == ctaptransport.CTAP2_ERR_NO_CREDENTIALS { + device.Close() + continue + } + device.Close() return nil, err } - // Try built-in user verification first (for devices that handle it - // on-device). libfido2 returns ErrPinRequired if a client PIN is needed. - assertion, err := device.Assertion( - i.relyingParty, - make([]byte, 32), - [][]byte{i.credentialID}, - "", - &libfido2.AssertionOpts{ - Extensions: []libfido2.Extension{libfido2.HMACSecretExtension}, - HMACSalt: hmacSecretSalt(nonce), - UV: libfido2.True, + salts := hmacSecretSalt(nonce) + extensions := &webauthn.GetAuthenticationExtensionsClientInputs{ + GetHMACSecretInputs: &webauthn.GetHMACSecretInputs{ + HMACGetSecret: webauthn.HMACGetSecretInput{ + Salt1: salts[:32], + Salt2: salts[32:], + }, }, - ) - if errors.Is(err, libfido2.ErrPinRequired) { + } + + var pinUvAuthToken []byte + var options map[protocol.Option]bool + cachedInfo, _ := device.GetInfoCached() + if cachedInfo.Options[protocol.OptionUserVerification] { + options = map[protocol.Option]bool{protocol.OptionUserVerification: true} + } else { pin, err := i.getPIN() if err != nil { + device.Close() return nil, err } - assertion, err = device.Assertion( - i.relyingParty, - make([]byte, 32), - [][]byte{i.credentialID}, + pinUvAuthToken, err = device.GetPinUvAuthTokenUsingPIN( + ctx, pin, - &libfido2.AssertionOpts{ - Extensions: []libfido2.Extension{libfido2.HMACSecretExtension}, - HMACSalt: hmacSecretSalt(nonce), - UV: libfido2.True, - }, + protocol.PermissionGetAssertion, + i.relyingParty, ) + if err != nil { + device.Close() + return nil, err + } + defer clear(pinUvAuthToken) + } + + for assertion, err = range device.GetAssertion( + ctx, + pinUvAuthToken, + i.relyingParty, + nil, + []credential.PublicKeyCredentialDescriptor{credentialDescriptor}, + extensions, + options, + ) { + break } if err != nil { + device.Close() return nil, err } - if assertion.HMACSecret == nil { - return nil, errors.New("FIDO2 device doesn't support HMACSecret extension") - } - return assertion.HMACSecret, nil + output := assertion.ExtensionOutputs.GetHMACSecretOutputs.HMACGetSecret + secret := make([]byte, 0, 64) + secret = append(secret, output.Output1...) + secret = append(secret, output.Output2...) + clear(output.Output1) + clear(output.Output2) + device.Close() + return secret, nil + } + if !deviceFound { + return nil, errors.New("no FIDO2 devices found") } - return nil, errors.New("identity doesn't match any FIDO2 device") } @@ -187,7 +368,9 @@ func (i *Identity) Unwrap(s []*age.Stanza) ([]byte, error) { return nil, err } key := hkdf.Extract(sha256.New, secret, []byte(label)) + clear(secret) fileKey, err := aeadDecrypt(key, 16, stanza.Body) + clear(key) if err != nil { continue } @@ -209,7 +392,9 @@ func (i *Identity) WrapWithLabels(fileKey []byte) ([]*age.Stanza, []string, erro return nil, nil, err } key := hkdf.Extract(sha256.New, secret, []byte(label)) + clear(secret) ciphertext, err := aeadEncrypt(key, fileKey) + clear(key) if err != nil { return nil, nil, err } diff --git a/go.mod b/go.mod index a2b809f..a8aac87 100644 --- a/go.mod +++ b/go.mod @@ -1,15 +1,23 @@ module filippo.io/typage -go 1.23.5 +go 1.27.0 require ( filippo.io/age v1.2.1-0.20240926110859-2214a556f604 - github.com/keys-pub/go-libfido2 v1.5.4-0.20250104233141-2534349bd685 + github.com/telesma-app/ctap v0.49.2 + github.com/telesma-app/pcsc v0.9.0 + golang.org/x/crypto v0.55.0 + golang.org/x/term v0.45.0 ) require ( - github.com/pkg/errors v0.9.1 // indirect - golang.org/x/crypto v0.24.0 - golang.org/x/sys v0.30.0 // indirect - golang.org/x/term v0.29.0 + github.com/cloudflare/circl v1.6.5 // indirect + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1 // indirect + github.com/ebitengine/purego v0.10.2 // indirect + github.com/fxamacker/cbor/v2 v2.9.3 // indirect + github.com/telesma-app/hid v0.12.1 // indirect + github.com/telesma-app/iso7816 v0.2.0 // indirect + github.com/x448/float16 v0.8.4 // indirect + golang.org/x/sys v0.47.0 // indirect + golang.org/x/text v0.41.0 // indirect ) diff --git a/go.sum b/go.sum index 98547e7..c9a68b4 100644 --- a/go.sum +++ b/go.sum @@ -2,24 +2,31 @@ c2sp.org/CCTV/age v0.0.0-20240306222714-3ec4d716e805 h1:u2qwJeEvnypw+OCPUHmoZE3I c2sp.org/CCTV/age v0.0.0-20240306222714-3ec4d716e805/go.mod h1:FomMrUJ2Lxt5jCLmZkG3FHa72zUprnhd3v/Z18Snm4w= filippo.io/age v1.2.1-0.20240926110859-2214a556f604 h1:LeljYZXJZFcoXQh8p+C5GGzI2A0M2mxaDileBHw3ch4= filippo.io/age v1.2.1-0.20240926110859-2214a556f604/go.mod h1:JL9ew2lTN+Pyft4RiNGguFfOpewKwSHm5ayKD/A4004= -github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/keys-pub/go-libfido2 v1.5.4-0.20250104233141-2534349bd685 h1:zSJ+NjvdW6SKXv9+EGfbaXYveyamZKw2SE2uJdURCMQ= -github.com/keys-pub/go-libfido2 v1.5.4-0.20250104233141-2534349bd685/go.mod h1:92J9LtSBl0UyUWljElJpTbMMNhC6VeY8dshsu40qjjo= -github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= -github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= -github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= -golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI= -golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= -golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= -golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU= -golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= -gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +github.com/cloudflare/circl v1.6.5 h1:O64F26HEqNhznd/hrC5KZXVKYuKM2rx4deZDTc4ihQA= +github.com/cloudflare/circl v1.6.5/go.mod h1:h5LNyxAc5nTue9DS5jT+48en2PSDYt3zdGnz5OstK6c= +github.com/decred/dcrd/crypto/blake256 v1.1.0 h1:zPMNGQCm0g4QTY27fOCorQW7EryeQ/U0x++OzVrdms8= +github.com/decred/dcrd/crypto/blake256 v1.1.0/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1 h1:5RVFMOWjMyRy8cARdy79nAmgYw3hK/4HUq48LQ6Wwqo= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40= +github.com/ebitengine/purego v0.10.2 h1:W809HbnvzAxgdm+aOvlSekrM16wGCdT/e76+9tS7gzE= +github.com/ebitengine/purego v0.10.2/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= +github.com/fxamacker/cbor/v2 v2.9.3 h1:oQBnFATpNdY8gJHTndDDv5Xl4QqNaz51G5LLEPhng3Q= +github.com/fxamacker/cbor/v2 v2.9.3/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= +github.com/telesma-app/ctap v0.49.2 h1:Wfq0Maa/kmgc0BdGlQEOkUxP4a6+J0V8yQzhz+B71Uo= +github.com/telesma-app/ctap v0.49.2/go.mod h1:mLfbRhwuIGdlYG5+qsqrAPTOYUgqb57BRqMZ5xUw2t4= +github.com/telesma-app/hid v0.12.1 h1:vj0bH7FzFw+eYY7VuTWDeg5lXE90lLvf/CRLItUmR8w= +github.com/telesma-app/hid v0.12.1/go.mod h1:bmzcOr5Fh1XVd/2Qn9VU0QIZkR3oFYnvnAb9NEqfqoY= +github.com/telesma-app/iso7816 v0.2.0 h1:Ny8ErRKmFOJ6IUcOdvw/HKjGOXATa//LUx/GHXlxT/M= +github.com/telesma-app/iso7816 v0.2.0/go.mod h1:oI7P4NERU4/SeBbou5UYg9vHiBvSC7QWF9PbLxcjxb4= +github.com/telesma-app/pcsc v0.9.0 h1:vrirJ9xMgJyPF41lG3wg4Te7fmA+46bnenF4aswjV8w= +github.com/telesma-app/pcsc v0.9.0/go.mod h1:a083kzMqXUCC2DgPzHtF/TZ2p1msMAK9DSx4wS245lY= +github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= +golang.org/x/crypto v0.55.0 h1:+KWHjbgOaAQ66dh/YlkZKHlz9ZUlq61AFirAR9ntP8M= +golang.org/x/crypto v0.55.0/go.mod h1:uq0V9dE/fzQuJtbnL+2EhWOE63vo164FY8xqEnV9xis= +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.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0= +golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w= +golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8= +golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M=