...
 
Commits (9)
......@@ -72,7 +72,7 @@ func TestLogging(t *testing.T) {
// Test max parameter
logs, err = client.LoadNewestLogs(1)
require.NoError(t, err)
require.True(t, len(logs) == oldLogLength+1)
require.True(t, len(logs) == 1)
// Do signature session
request = getSigningRequest(attrid)
......
......@@ -120,7 +120,7 @@ func TestLargeAttribute(t *testing.T) {
client, _ := parseStorage(t)
defer test.ClearTestStorage(t)
require.NoError(t, client.RemoveAllCredentials())
require.NoError(t, client.RemoveStorage())
issuanceRequest := getSpecialIssuanceRequest(false, "1234567890123456789012345678901234567890") // 40 chars
sessionHelper(t, issuanceRequest, "issue", client)
......@@ -204,7 +204,7 @@ that they have been fixed. */
func TestAttributeByteEncoding(t *testing.T) {
client, _ := parseStorage(t)
defer test.ClearTestStorage(t)
require.NoError(t, client.RemoveAllCredentials())
require.NoError(t, client.RemoveStorage())
/* After bitshifting the presence bit into the large attribute below, the most significant
bit is 1. In the bigint->[]byte conversion that happens before hashing this attribute, in
......@@ -229,7 +229,7 @@ func TestOutdatedClientIrmaConfiguration(t *testing.T) {
defer test.ClearTestStorage(t)
// Remove old studentCard credential from before support for optional attributes, and issue a new one
require.NoError(t, client.RemoveAllCredentials())
require.NoError(t, client.RemoveStorage())
require.Nil(t, requestorSessionHelper(t, getIssuanceRequest(true), client).Err)
// client does not have updated irma_configuration with new attribute irma-demo.RU.studentCard.newAttribute,
......@@ -249,7 +249,7 @@ func TestDisclosureNewAttributeUpdateSchemeManager(t *testing.T) {
require.False(t, client.Configuration.CredentialTypes[credid].ContainsAttribute(attrid))
// Remove old studentCard credential from before support for optional attributes, and issue a new one
require.NoError(t, client.RemoveAllCredentials())
require.NoError(t, client.RemoveStorage())
require.Nil(t, requestorSessionHelper(t, getIssuanceRequest(true), client).Err)
// Trigger downloading the updated irma_configuration using a disclosure request containing the
......
......@@ -331,33 +331,34 @@ func (client *Client) RemoveCredentialByHash(hash string) error {
return client.RemoveCredential(cred.CredentialType().Identifier(), index)
}
// RemoveAllCredentials removes all credentials.
func (client *Client) RemoveAllCredentials() error {
removed := map[irma.CredentialTypeIdentifier][]irma.TranslatedString{}
for _, attrlistlist := range client.attributes {
for _, attrs := range attrlistlist {
if attrs.CredentialType() != nil {
removed[attrs.CredentialType().Identifier()] = attrs.Strings()
}
}
// Removes all attributes, signatures, logs and userdata
// Includes the user's secret key, keyshare servers and preferences/updates
// A fresh secret key is installed.
func (client *Client) RemoveStorage() error {
var err error
// Remove data from memory
client.attributes = make(map[irma.CredentialTypeIdentifier][]*irma.AttributeList)
client.keyshareServers = make(map[irma.SchemeManagerIdentifier]*keyshareServer)
client.credentialsCache = make(map[irma.CredentialTypeIdentifier]map[int]*credential)
if err = client.storage.DeleteAll(); err != nil {
return err
}
// Client assumes there is always a secret key, so we have to load a new one
client.secretkey, err = client.storage.LoadSecretKey()
if err != nil {
return err
}
client.attributes = map[irma.CredentialTypeIdentifier][]*irma.AttributeList{}
logentry := &LogEntry{
Type: ActionRemoval,
Time: irma.Timestamp(time.Now()),
Removed: removed,
// TODO: do we consider this setting as user data?
if client.Preferences, err = client.storage.LoadPreferences(); err != nil {
return err
}
client.applyPreferences()
return client.storage.Transaction(func(tx *transaction) error {
if err := client.storage.TxDeleteAllAttributes(tx); err != nil {
return err
}
if err := client.storage.TxDeleteAllSignatures(tx); err != nil {
return err
}
return client.storage.TxAddLogEntry(tx, logentry)
})
return nil
}
// Attribute and credential getter methods
......
......@@ -3,14 +3,15 @@ package irmaclient
import (
"encoding/json"
"errors"
"github.com/privacybydesign/irmago/internal/fs"
"os"
"path/filepath"
"testing"
"github.com/privacybydesign/irmago/internal/fs"
"github.com/privacybydesign/gabi"
"github.com/privacybydesign/gabi/big"
"github.com/privacybydesign/irmago"
irma "github.com/privacybydesign/irmago"
"github.com/privacybydesign/irmago/internal/test"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
......@@ -45,8 +46,8 @@ func parseExistingStorage(t *testing.T) *Client {
func parseDisclosure(t *testing.T) (*Client, *irma.DisclosureRequest, *irma.Disclosure) {
client := parseStorage(t)
requestJson := `{"@context":"https://irma.app/ld/request/disclosure/v2","context":"AQ==","nonce":"M3LYmTr3CZDYZkMNK2uCCg==","protocolVersion":"2.5","disclose":[[["irma-demo.RU.studentCard.studentID"]]],"labels":{"0":null}}`
dislosureJson := `{"proofs":[{"c":"l1WDHGtsbEO+rVhVoGBDpzluiU5riCKtgMu6Mn4zxDg=","A":"XRyyZFL5xcvQDrCEoIchQdd1qyGpMIafNoak/8aSZisQ5U7JEa54Yu8nW4L9/4fXqLDK1SyX/CvFXrELbFBX1qf1lJ19jTViU9jIpSOw3D8w/DeY7Kg0evwVKUQrcrJnT3ss8J5gM5eRF1E1AuRHgKywWYvtxFvHQs2ODN2qsWY=","e_response":"m41dWZjTVYN6RqnojdHwgfixZwBJKW189b/ehnG3YTt0dMKDUnrLBYhGyKtmstnLzYTuJaBDX4r8","v_response":"DajHvzCDcmxXvd4sucgnrOkaOyFaF0EcOf23ySy56SAiFzWBW1BkcMQ8AwjwnVzYS5vpHnkUDkgqovOsl74RJQMSdnjzu0URAvGZm7/3pXgBjR5Q0154oMC3+n19pQrX68xgEOK7Am6jflfNufyINIVOAm7SfObsjKRDMQcuHOLgoj5XIHPJ3EBJcFJzizmaaGuGHKEJ0+b4Zi8JCBMaP9mdDhsUJAtm190hYxcMb2CtIIiJqGRk+JcNmusRuJYcT9OLx/Xklj+qm6/5C0+jRQPdYNycVzwKel+HDWZyYymCSpjbR5mUw1IpK1QvszN5NIJXVCeDrMMRZcySfOUA","a_responses":{"0":"xxlDTyJ1xq6TuMYgiyisNJ82tiJsnFdBinGP5ZQtw7rxXcLrO6k7nE88wPDuejzEnF7+LgIes32BMC+Qr/C/qh//x7SuMxDujoY=","2":"8HEFx5JJ24Z/D6MRtE6m7Pyk9T61S7lnxdTaych7wEK3ZO+4qyFYVZwx4NLtTp1MRfTiUq6KhNd7Is2cEBAdZYaL3XBnNRQMNvw=","3":"2BleNpicu21GFR1kYJ6kpFct6pyFSYz8hw5tBHtGz7O54KgHySwZ6lI/J4hp1b5l3RWq6gZzlz/PzOLKxk3E3YOwS7e4hsQ7BFo=","5":"977MbEQ95ieN/lVJSUS5Y80nY5KigNtrId2RW87CIsCZQ892rPljuZ0s/UG16b3oEYFEx+WZPxKvGiQN0dJiB8BK3P8qPZlGIu4="},"a_disclosed":{"1":"AgAJuwB+AALWy2qU9p3l52l9LU1rVT4M","4":"NDU2"}}],"indices":[[{"cred":0,"attr":4}]]}`
requestJson := `{"@context":"https://irma.app/ld/request/disclosure/v2","context":"AQ==","nonce":"zVQJMG6TKZwfcv5TExFVSQ==","protocolVersion":"2.5","disclose":[[["irma-demo.RU.studentCard.studentID"]]],"labels":{"0":null}}`
dislosureJson := `{"proofs":[{"c":"o21UPItMKWXmXNhBKsCBHDWjfRoy+uDdbDB1yhhpg3k=","A":"Bl68Ut2nu2nwhIweU9QGoNd6TkjUIRbQ6SDg22m8PzMEgca0KA4/Oy1gaJCUHM3FFJ0Gdj0+6/VpcF85JyuQZou93UXXwzN/Y7ohUw+YxVTQ7WcJmZ/VGDh3SME5KJ9aWjGmq61J2LQiiDSq+XrcWFfKPwad6BkDhV2reo4yo68=","e_response":"VD0pWdeDkd3V+R3734xyRcGeWMMTzpB0ZiJhKMzv37DmHN6RpRzTF/0HroAsMIMz8mBWxYPVRBiw","v_response":"3OWsmIDM7v0ByEXax2YZGp3BnJ5nkCLMcT6/ENU0EcpjrOz+rT+NayQSLgMshxAATpgkgAluFQ3owOoQEL8ZAkZTWUDW5j+qy7GDFd22ZOKEZLWf8Q1XRK3x6exV9CIMkcBQrv5W6EI9XB5OKKNB3Z/VTALY3UW8cQQ0DPHj83YBEL3LJQDxwaxvQeHx4nysJjsEoLJE1KPBynXlfxpk17O3HTg+NuX5gj7+ckiHrmXgthJHvqCTnNpEORtXDJTmKJUccUiyWuftA36cIXIxW4N6I88T4BYctwN+T9NY+hcjYESITtxB+r2elB98bzlWgHF8ohpOkkJGuNjTFjw=","a_responses":{"0":"eDQA3Lrh2WC3o/VP6KD/uaMSRy/em3gEfuqXD9tVT+yJFYb7GT91lle5dB6lg235pUSHzYIOET7FYOHwb4/YSAGQiix0IzqFkLo=","2":"kT3kfcIaPy3UBYPX78X10w/R1Cb5rHqoW5OUd06xqC1V9MqVw3zhtc/nBgWmvVwTgJrl2CyuBjjoF10RJz/FEjYZ0JAF57uUXW8=","3":"4oSBcyUT6mOBhk/Szk/5G5QrgaAADW6wSl91hGwTTNDTIUiK01GE11JozbwDeZsLPoFikzikwkPu9ZsOAtOtb/+IcadB6NP0KXA=","5":"OwUSSCBb9NOMOYYSGSYCrdFUNLKJ/b2YP5LlElFG5r4GPR71zTQsZ4QuJiMIt9iFPRP6PQUvMvjWA59UTQ9AlwKc9JcQzbScYBM="},"a_disclosed":{"1":"AwAKOQIBAALWy2qU9p3l52l9LU1rVT4M","4":"aGpt"}}],"indices":[[{"cred":0,"attr":4}]]}`
request := &irma.DisclosureRequest{}
require.NoError(t, json.Unmarshal([]byte(requestJson), request))
disclosure := &irma.Disclosure{}
......@@ -361,6 +362,31 @@ func TestUpdatingStorage(t *testing.T) {
}
}
func TestRemoveStorage(t *testing.T) {
client := parseStorage(t)
defer test.ClearTestStorage(t)
bucketsBefore := map[string]bool{"attrs": true, "sigs": true, "userdata": true, "logs": false} // Test storage has no logs
bucketsAfter := map[string]bool{"attrs": false, "sigs": false, "userdata": true, "logs": false} // Userdata should hold a new secret key
old_sk := *client.secretkey
// Check that buckets exist
for name, exists := range bucketsBefore {
require.Equal(t, exists, client.storage.BucketExists([]byte(name)))
}
require.NoError(t, client.RemoveStorage())
for name, exists := range bucketsAfter {
require.Equal(t, exists, client.storage.BucketExists([]byte(name)))
}
// Check that the client has a new secret key
new_sk := *client.secretkey
require.NotEqual(t, old_sk, new_sk)
}
// ------
type TestClientHandler struct {
......
......@@ -65,6 +65,15 @@ func (s *storage) Close() error {
return s.db.Close()
}
func (s *storage) BucketExists(name []byte) bool {
return s.db.View(func(tx *bbolt.Tx) error {
if tx.Bucket(name) == nil {
return bbolt.ErrBucketNotFound
}
return nil
}) == nil
}
func (s *storage) txStore(tx *transaction, bucketName string, key string, value interface{}) error {
b, err := tx.CreateBucketIfNotExists([]byte(bucketName))
if err != nil {
......@@ -339,3 +348,33 @@ func (s *storage) LoadPreferences() (Preferences, error) {
_, err := s.load(userdataBucket, preferencesKey, &config)
return config, err
}
func (s *storage) TxDeleteUserdata(tx *transaction) error {
return tx.DeleteBucket([]byte(userdataBucket))
}
func (s *storage) TxDeleteLogs(tx *transaction) error {
return tx.DeleteBucket([]byte(logsBucket))
}
func (s *storage) TxDeleteAll(tx *transaction) error {
if err := s.TxDeleteAllAttributes(tx); err != nil && err != bbolt.ErrBucketNotFound {
return err
}
if err := s.TxDeleteAllSignatures(tx); err != nil && err != bbolt.ErrBucketNotFound {
return err
}
if err := s.TxDeleteUserdata(tx); err != nil && err != bbolt.ErrBucketNotFound {
return err
}
if err := s.TxDeleteLogs(tx); err != nil && err != bbolt.ErrBucketNotFound {
return err
}
return nil
}
func (s *storage) DeleteAll() error {
return s.Transaction(func(tx *transaction) error {
return s.TxDeleteAll(tx)
})
}
......@@ -104,6 +104,10 @@ const (
privkeyPattern = "%s/%s/%s/PrivateKeys/*.xml"
)
var (
validLangs = []string{"en", "nl"} // Hardcode these for now, TODO make configurable
)
func (sme SchemeManagerError) Error() string {
return fmt.Sprintf("Error parsing scheme manager %s: %s", sme.Manager.Name(), sme.Err.Error())
}
......@@ -1313,6 +1317,15 @@ func (conf *Configuration) StopAutoUpdateSchemes() {
}
// Validation methods containing consistency checks on irma_configuration
func validateDemoPrefix(ts TranslatedString) error {
prefix := "Demo "
for _, lang := range validLangs {
if !strings.HasPrefix(map[string]string(ts)[lang], prefix) {
return errors.Errorf("value in language %s is not prefixed with '%s'", lang, prefix)
}
}
return nil
}
func (conf *Configuration) validateIssuer(manager *SchemeManager, issuer *Issuer, dir string) error {
issuerid := issuer.Identifier()
......@@ -1333,6 +1346,9 @@ func (conf *Configuration) validateIssuer(manager *SchemeManager, issuer *Issuer
if manager.ID != issuer.SchemeManagerID {
return errors.Errorf("Issuer %s has wrong SchemeManager %s", issuerid.String(), issuer.SchemeManagerID)
}
if err = validateDemoPrefix(issuer.Name); manager.Demo && err != nil {
return errors.Errorf("Name of demo issuer %s invalid: %s", issuer.ID, err.Error())
}
if err = fs.AssertPathExists(filepath.Join(dir, "logo.png")); err != nil {
conf.Warnings = append(conf.Warnings, fmt.Sprintf("Issuer %s has no logo.png", issuerid.String()))
}
......@@ -1354,6 +1370,9 @@ func (conf *Configuration) validateCredentialType(manager *SchemeManager, issuer
if cred.SchemeManagerID != manager.ID {
return errors.Errorf("Credential type %s has wrong SchemeManager %s", credid.String(), cred.SchemeManagerID)
}
if err := validateDemoPrefix(cred.Name); manager.Demo && err != nil {
return errors.Errorf("Name of demo credential %s invalid: %s", cred.ID, err.Error())
}
if err := fs.AssertPathExists(filepath.Join(dir, "logo.png")); err != nil {
conf.Warnings = append(conf.Warnings, fmt.Sprintf("Credential type %s has no logo.png", credid.String()))
}
......@@ -1406,7 +1425,6 @@ func (conf *Configuration) validateScheme(scheme *SchemeManager, dir string) err
// validateTranslations checks for each member of the interface o that is of type TranslatedString
// that it contains all necessary translations.
func (conf *Configuration) validateTranslations(file string, o interface{}) {
langs := []string{"en", "nl"} // Hardcode these for now, TODO make configurable
v := reflect.ValueOf(o)
// Dereference in case of pointer or interface
......@@ -1421,7 +1439,7 @@ func (conf *Configuration) validateTranslations(file string, o interface{}) {
continue
}
val := field.Interface().(TranslatedString)
for _, lang := range langs {
for _, lang := range validLangs {
if _, exists := val[lang]; !exists {
conf.Warnings = append(conf.Warnings, fmt.Sprintf("%s misses %s translation in <%s> tag", file, lang, name))
}
......
......@@ -312,9 +312,14 @@ func (s *Server) handleCreate(w http.ResponseWriter, r *http.Request) {
return
}
if !applies {
s.conf.Logger.Warnf("Session request uses unknown authentication method, HTTP headers: %s, HTTP POST body: %s",
server.ToJson(r.Header), string(body))
server.WriteError(w, server.ErrorInvalidRequest, "Request could not be authorized")
var ctype = r.Header.Get("Content-Type")
if ctype != "application/json" && ctype != "text/plain" {
s.conf.Logger.Warnf("Session request uses unsupported Content-Type: %s", ctype)
server.WriteError(w, server.ErrorInvalidRequest, "Unsupported Content-Type: "+ctype)
return
}
s.conf.Logger.Warnf("Session request uses unknown authentication method, HTTP headers: %s, HTTP POST body: %s", server.ToJson(r.Header), string(body))
server.WriteError(w, server.ErrorInvalidRequest, "Request could not be authenticated")
return
}
......
[{"Ints":[49043497911096929087709751855583038580649425750938808456,6713199]},{"Ints":[49043497911096929607726931703203423024551950578089278988,23188031866369380,3224115,3421494,5269572]}]
\ No newline at end of file
[{"Ints":["AwAKOQIBAANPk8AhXLlPUlSw2hYiAvCI","6Mrm6OrmyuTcwtrL"]},{"Ints":["AwAKOQIBAALWy2qU9p3l52l9LU1rVT4M","pMLIxN7qyQ==","YmRn","aGpt","aGU="]}]
\ No newline at end of file
{"A":45222154268674273710241606712088428574327878157177201139384374721171826461412293659115700357192323970285281190820411677649364221414391111849580700591248745148446158534302920406192967484495995535298878524558192472720010597489395900822557876058305418130894199313225355725563905329921324127951328609858221865688,"e":259344723055062059907025491480697571938277889515152306249728583105665800713306759149981690559193987143012367913206299323899696942213235956742930189984109555429399234387572652290181,"v":37089426574404853314527538425590154464185669611257826737975997075833182365911745361173581237316274982025210017363868383021489436792111663380955676867084118464865738380944058955231525097633962671227066163548711934512602987866484024140781599202158747051737882065397224229528273128088731188352989738315756064211230068049165954427138829408447136906870620401460509477728657639517925540509852663699685295279125577115556094945251824705694439002539764689968101329433910130152847508956031277000706585794137651236318227128,"KeyshareP":85082025162837201194376665159280932833709502354228382426819253775157607330761605538885353580375270262824918717451102191200845300689661501037194721171086025783504808921909133839891286893734098605996576876074961701903214263017801474730299291177947936831353263293658665257545333300975086052644477159922153700211}
\ No newline at end of file
{"A":"SbgMyYglj5e3O6rD1OE1N056q2VSJXpF+sAMwd2RFubiAP9bOejlvvl8cY0QXKRAeY10UwWeB71RkNkLNgZvdw2xnQAOTsZIkoo9gI2lkviaO8TjuBVsnZf3aK7SNb+HxyE6LLZFxFzEhuLzGs0SBQQNXqLuojOFXO9SVQkEeYo=","e":"EAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfWpIyCaadRIDXSQT51wl","v":"DOPtugK674cgZiD4axaFmnNDwAExTNN3XeJqFjJG+MD+KU+loR9O7kgENpdQITYf4NdQggqbrcynvgAlXcyw9wX7iEuRPTs4xgdd6N7i602pOLQJyo0hrP3485kNp6SM9ybx0MVHm2qvqo5u121UmFLG8bXU+aP5PdjFBv5QU4ctix0kN3J9CkQZtw6Ep4JfuLrAI7PjmqLogh70klHdb4JxNWyvRGnoHi8cxIfxVmfsRd+XCLtsZwP6/dZDQl0uObjj4Put2qCuVIzsfmzeu26xY9G6","KeyshareP":null}
\ No newline at end of file
{"A":19361989720240022780364031292670736589482375295351376840389602869290921590081983014245433147598934021651062476173549660545604024910603570251193965749831813974082815089185920569764627738209782651392073734649038157462687171908062832755984081636063543276779686036825699213396941509489370406466435663161305944357,"e":259344723055062059907025491480697571938277889515152306249728583105665800713306759149981690559193987143012367913206299323899696942213235956742930225372495635518051503820864567017509,"v":44504154960997969936163816284867421689230969904501819714082539974281416125028394649804074185982730620713368510527561786489151242878408608924108769812084146129727784097430924126631729595258408129362124864052428072634924023503654055028585711391218571487914853282274713963805294744852479094032534022811676406774207262761522055883100899856663354161806256075693680140216792420850477388497107343316133929461954334492914769220324629269238945857169205525679465959160495511815055891870320672867787549146029373683233228360,"KeyshareP":null}
\ No newline at end of file
{"A":"Fey+YGwfFZGN+ySjElWNnmdSuRU5iO7jsf+NXgufqWU8vW6MUDnoLHA1CNif7+OSXXPi8GT/2N5Hct9arX23y1+9XeYHOvp0ABknUDG+2D9RdYpTJVePsw8bJxVKb1tk6VvXhK1LMaTr7IdQ8BcMOWrOtNmKty80C77+Jeyv+rhACnTojre4C6IO577KzFRzWnGO+ZrHFdTOMvulZ8710IzB+lPEgnr+rYf1L+P3N5D2JbzHtWQUmLqUxRRf3E4Bs8gdXMljeSVTZptZedoMcY/6mjxv84nyap8K4sPewPBYQF2bguItu3HWqmb9lA4oi2c5/pPVBU157EuafaPyjw==","e":"EAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHMGbdrY3qxDwFP2JA0MJ","v":"Cvo4Jz+GSESgy3Ozz+rPY+zIPFxVPIYDy50lJZv5Wjzw9YkPPNysRHgqzjcy1u6UQG020Ogutdw5+rijGqmHbvtIvRYI4zDv/zJNQQEkvFXHe+VR2hLW86xAyoEW9G7ND0lyzjuJ70ZOOV8JhjuDDMpTt/Ir1i1G0I/RvCckwTznyMHfzf3VfwdlTYZuT1VxN/pEqTk+F+EWREFvvgZPuM8BOHBjQcFhvHxk5B4NOd29Q9xb5UE7xr4OQNbIPmahSO6xiASE+GoA1VgakzG8JjL+CNoWixvUYyMm0C8TiskoWrctct7MMfM7odUAIa/6yx1hjLN6OgcIl6AF0kdtE/Ym1lVI1ZdB2dgN2oakFJiExRX4UkoZM4ZwvjaLwPvweUpv3Kz/of863wvdcLHPrFGGWvKGIIYe50Vle2x5uTVMVCFwQSr7vw/X6QPrZP3kk2INQhMlDQ7fx6UNBIb+KVg=","KeyshareP":"pwFgf8l5q/iFiqzLzeEukWjfg1hn3QsMl1yjuUFJvFfVn2xOf2I9sxK1WG1NcesToQJ5RqVHdt/D4A2gybfuUXfFEQ6jjHAu8g8b54EMh2yoKBkepWtDQXudlR7Mj4lPjNwwo11VuyGdu6Ym/B9ezbBkSzMvJN+Mi3k3sdm3PBf8oIQ8mIZiEkv7/kBo7dxAai6b2MjRlvn8MHSCxrQUMvW9D8y3PjwKSYjsdyxyvR3AOvwjbV+qikVz+kqX0BqvLfwBmrukchjpXu9Bhamj6TkgFfjPzoXYcAenH0UYyWaL0VPrOZOaKAPzZvvx1+k7FMK+T+HJwwgvueL3Mc4U6Q=="}
\ No newline at end of file
No preview for this file type