Skip to content
Snippets Groups Projects
storage.go 9.55 KiB
Newer Older
package irmago

import (
	"encoding/json"
Sietse Ringers's avatar
Sietse Ringers committed
	"crypto/rand"
	"encoding/hex"
	"math/big"
	"path"

	"github.com/mhe/gabi"
)

// Filenames in which we store stuff
const (
	skFile         = "sk"
	attributesFile = "attrs"
	kssFile        = "kss"
	paillierFile   = "paillier"
Sietse Ringers's avatar
Sietse Ringers committed
	cardemuXML     = "../cardemu.xml"
Sietse Ringers's avatar
Sietse Ringers committed
// PathExists checks if the specified path exists.
func PathExists(path string) (bool, error) {
	_, err := os.Stat(path)
	if err == nil {
		return true, nil
	}
	if os.IsNotExist(err) {
		return false, nil
	}
	return true, err
}

// Init deserializes the credentials from storage.
func (cm *CredentialManager) Init(path string, keyshareHandler KeyshareHandler) (err error) {
	cm.storagePath = path

	err = cm.ensureStorageExists()
	if err != nil {
		return err
	}
	cm.secretkey, err = cm.loadSecretKey()
	if err != nil {
		return
	}
	cm.attributes, err = cm.loadAttributes()
	if err != nil {
		return
	}
Sietse Ringers's avatar
Sietse Ringers committed
	cm.paillierKeyCache, err = cm.loadPaillierKeys()
	if err != nil {
		return
	}

	cm.keyshareServers, err = cm.loadKeyshareServers()
	if err != nil {
		return
	}
Sietse Ringers's avatar
Sietse Ringers committed

	unenrolled := cm.unenrolledKeyshareServers()
	switch len(unenrolled) {
	case 0:
		return
	case 1:
		if keyshareHandler == nil {
			return errors.New("Keyshare server found but no KeyshareHandler was given")
		keyshareHandler.StartKeyshareRegistration(unenrolled[0], func(email, pin string) {
			cm.KeyshareEnroll(unenrolled[0].Identifier(), email, pin)
		})
Sietse Ringers's avatar
Sietse Ringers committed
	default:
		return errors.New("Too many keyshare servers")
	}

	return nil
}

// ParseAndroidStorage parses an Android cardemu.xml shared preferences file
// from the old Android IRMA app, parsing its credentials into the current instance,
// and saving them to storage.
// CAREFUL: this method overwrites any existing secret keys and attributes on storage.
func (cm *CredentialManager) ParseAndroidStorage() (err error) {
	exists, err := PathExists(cm.path(cardemuXML))
	if err != nil || !exists {
		return
	}

	bytes, err := ioutil.ReadFile(cm.path(cardemuXML))
	parsedxml := struct {
		Strings []struct {
			Name    string `xml:"name,attr"`
			Content string `xml:",chardata"`
		} `xml:"string"`
	}{}
	xml.Unmarshal(bytes, &parsedxml)

	parsedjson := make(map[string][]*gabi.Credential)
	parsedksses := make(map[string]*keyshareServer)
	for _, xmltag := range parsedxml.Strings {
		if xmltag.Name == "credentials" {
			jsontag := html.UnescapeString(xmltag.Content)
			if err = json.Unmarshal([]byte(jsontag), &parsedjson); err != nil {
				return
			}
		}
		if xmltag.Name == "keyshare" {
			jsontag := html.UnescapeString(xmltag.Content)
			if err = json.Unmarshal([]byte(jsontag), &parsedksses); err != nil {
				return
			}
		}
		if xmltag.Name == "KeyshareKeypairs" {
			jsontag := html.UnescapeString(xmltag.Content)
			keys := make([]*paillierPrivateKey, 0, 3)
			if err = json.Unmarshal([]byte(jsontag), &keys); err != nil {
				return
			}
			cm.paillierKeyCache = keys[0]
		}
	}

	for name, kss := range parsedksses {
		cm.keyshareServers[NewSchemeManagerIdentifier(name)] = kss
	}

	for _, list := range parsedjson {
		cm.secretkey = list[0].Attributes[0]
		for i, gabicred := range list {
			cred := newCredential(gabicred)
			if cred.CredentialType() == nil {
				return errors.New("cannot add unknown credential type")
			}

			cm.addCredential(cred)
			err = cm.storeSignature(cred, i)
			if err != nil {
				return err
			}
		}
	}

	if len(cm.credentials) > 0 {
		err = cm.storeAttributes()
		if err != nil {
			return err
		}
		err = cm.storeSecretKey(cm.secretkey)
		if err != nil {
			return err
		}
	}

	if len(cm.keyshareServers) > 0 {
		err = cm.storeKeyshareServers()
		if err != nil {
			return err
		}
	}

	err = cm.storePaillierKeys()
	if err != nil {
		return err
	}
	if cm.paillierKeyCache == nil {
		cm.paillierKey() // trigger calculating a new one
	}

	return
}

func (cm *CredentialManager) path(file string) string {
	return cm.storagePath + "/" + file
}

Sietse Ringers's avatar
Sietse Ringers committed
func (cm *CredentialManager) signatureFilename(id string, counter int) string {
	return cm.path(signaturesDir) + "/" + id + "-" + strconv.Itoa(counter)
}

// ensureStorageExists initializes the credential storage folder,
// ensuring that it is in a usable state.
// NOTE: we do not create the folder if it does not exist!
// Setting it up in a properly protected location (e.g., with automatic
// backups to iCloud/Google disabled) is the responsibility of the user.
func (cm *CredentialManager) ensureStorageExists() (err error) {
	exist, err := PathExists(cm.storagePath)
	if err != nil {
		return
	}
	if !exist {
		return errors.New("credential storage path does not exist")
	}

	exist, err = PathExists(cm.path(signaturesDir))
	if err != nil {
		return err
	}
	if !exist {
		err = os.Mkdir(cm.path(signaturesDir), 0700)
	}

	return
}

Sietse Ringers's avatar
Sietse Ringers committed
func (cm *CredentialManager) storeSecretKey(sk *big.Int) error {
	return ioutil.WriteFile(cm.path(skFile), sk.Bytes(), 0600)
}

// Save the filecontents at the specified path atomically:
// - first save the content in a temp file with a random filename in the same dir
// - then rename the temp file to the specified filepath, overwriting the old file
func (cm *CredentialManager) saveFile(filepath string, content []byte) (err error) {
	dir := path.Dir(filepath)

	// Read random data for filename and convert to hex
	randBytes := make([]byte, 16)
	_, err = rand.Read(randBytes)
	if err != nil {
		return
	}
	tempfilename := hex.EncodeToString(randBytes)

	// Create temp file
	err = ioutil.WriteFile(dir+"/"+tempfilename, content, 0600)
	if err != nil {
		return
	}

	// Rename, overwriting old file
	return os.Rename(dir+"/"+tempfilename, filepath)
Sietse Ringers's avatar
Sietse Ringers committed
func (cm *CredentialManager) storeSignature(cred *credential, counter int) (err error) {
	if cred.CredentialType() == nil {
		return errors.New("cannot add unknown credential type")
	}

	credbytes, err := json.Marshal(cred.Signature)
	if err != nil {
		return err
	}

	// TODO existence check
	filename := cm.signatureFilename(cred.CredentialType().Identifier().String(), counter)
	err = ioutil.WriteFile(filename, credbytes, 0600)
	return
}

func (cm *CredentialManager) storeAttributes() (err error) {
	// Unfortunately, the type of cm.attributes (map[CredentialTypeIdentifier][]*AttributeList)
	// cannot be passed directly to json.Marshal(), so we copy it into a temp list.
	temp := make(map[string][]*AttributeList)
	for credid, list := range cm.attributes {
		temp[credid.String()] = list
	}
	attrbytes, err := json.Marshal(temp)
	}

	// TODO existence check
	err = ioutil.WriteFile(cm.path(attributesFile), attrbytes, 0600)
	return
}
func (cm *CredentialManager) storeKeyshareServers() (err error) {
	temp := make(map[string]*keyshareServer)
	for name, kss := range cm.keyshareServers {
		temp[name.String()] = kss
	}
	bts, err := json.Marshal(temp)
	if err != nil {
		return
	}
	err = ioutil.WriteFile(cm.path(kssFile), bts, 0600)
	return
}

func (cm *CredentialManager) storePaillierKeys() (err error) {
	bts, err := json.Marshal(cm.paillierKeyCache)
	if err != nil {
		return
	}
	err = ioutil.WriteFile(cm.path(paillierFile), bts, 0600)
	return
}

func (cm *CredentialManager) loadSignature(id CredentialTypeIdentifier, counter int) (signature *gabi.CLSignature, err error) {
Sietse Ringers's avatar
Sietse Ringers committed
	sigpath := cm.signatureFilename(id.String(), counter)
	exists, err := PathExists(sigpath)
Sietse Ringers's avatar
Sietse Ringers committed
	if err != nil || !exists {
		return
	}
Sietse Ringers's avatar
Sietse Ringers committed
	bytes, err := ioutil.ReadFile(sigpath)
Sietse Ringers's avatar
Sietse Ringers committed
	signature = new(gabi.CLSignature)
	err = json.Unmarshal(bytes, signature)
	return
}

// loadSecretKey retrieves and returns the secret key from storage, or if no secret key
// was found in storage, it generates, saves, and returns a new secret key.
func (cm *CredentialManager) loadSecretKey() (*big.Int, error) {
	exists, err := PathExists(cm.path(skFile))
Sietse Ringers's avatar
Sietse Ringers committed
	if err != nil {
		return nil, err
	}
	if exists {
		var bytes []byte
		if bytes, err = ioutil.ReadFile(cm.path(skFile)); err == nil {
			return new(big.Int).SetBytes(bytes), nil

	sk, err := cm.generateSecretKey()
	if err != nil {
		return nil, err
	}
	err = cm.storeSecretKey(sk)
	if err != nil {
		return nil, err
	}
	return sk, nil
func (cm *CredentialManager) loadAttributes() (list map[CredentialTypeIdentifier][]*AttributeList, err error) {
	list = make(map[CredentialTypeIdentifier][]*AttributeList)
	temp := make(map[string][]*AttributeList)
	exists, err := PathExists(cm.path(attributesFile))
Sietse Ringers's avatar
Sietse Ringers committed
	if err != nil || !exists {
		return
	}

	bytes, err := ioutil.ReadFile(cm.path(attributesFile))
	if err != nil {
		return nil, err
	}
	err = json.Unmarshal(bytes, &temp)
	if err != nil {
		return nil, err
	}
	for credid, attrs := range temp {
		list[NewCredentialTypeIdentifier(credid)] = attrs

func (cm *CredentialManager) loadKeyshareServers() (ksses map[SchemeManagerIdentifier]*keyshareServer, err error) {
	ksses = make(map[SchemeManagerIdentifier]*keyshareServer)
	temp := make(map[string]*keyshareServer)

	exists, err := PathExists(cm.path(kssFile))
	if err != nil || !exists {
		return
	}
	bytes, err := ioutil.ReadFile(cm.path(kssFile))
	if err != nil {
		return nil, err
	}
	err = json.Unmarshal(bytes, &temp)
	if err != nil {
		return nil, err
	}
	for name, kss := range temp {
		ksses[NewSchemeManagerIdentifier(name)] = kss
	}
	return
}

func (cm *CredentialManager) loadPaillierKeys() (key *paillierPrivateKey, err error) {
	exists, err := PathExists(cm.path(paillierFile))
	if err != nil || !exists {
		return
	}
	bytes, err := ioutil.ReadFile(cm.path(paillierFile))
	if err != nil {
		return nil, err
	}
	key = new(paillierPrivateKey)
	err = json.Unmarshal(bytes, key)
	if err != nil {
		return nil, err
	}
	return
}