Commit 1f6763ab authored by Sietse Ringers's avatar Sietse Ringers
Browse files

Add credential loading

parent 205d5943
......@@ -2,24 +2,69 @@ package irmago
import "testing"
import (
"os"
"fmt"
"github.com/mhe/gabi"
"github.com/stretchr/testify/assert"
)
func TestAndroidParse(t *testing.T) {
gabi.MetaStore.ParseFolder("testdata/irma_configuration")
Manager.Init("testdata/storage")
err := Manager.ParseAndroidStorage()
func TestMain(m *testing.M) {
if len(gabi.MetaStore.SchemeManagers) == 0 { // FIXME
gabi.MetaStore.ParseFolder("testdata/irma_configuration")
}
Manager = newCredentialManager()
err := os.RemoveAll("testdata/storage/test")
if err != nil {
fmt.Errorf("Could not delete test storage")
os.Exit(1)
}
err = os.Mkdir("testdata/storage/test", 0755)
if err != nil {
fmt.Errorf("Could not create test storage")
os.Exit(1)
}
retCode := m.Run()
err = os.RemoveAll("testdata/storage/test")
if err != nil {
fmt.Errorf("Could not delete test storage")
os.Exit(1)
}
os.Exit(retCode)
}
func parseAndroidStorage(t *testing.T) {
err := Manager.Init("testdata/storage/test")
assert.NoError(t, err, "Manager.Init() failed")
err = Manager.ParseAndroidStorage()
assert.NoError(t, err, "ParseAndroidStorage failed")
assert.NotEmpty(t, Manager.credentials, "No credentials deserialized")
assert.Contains(t, Manager.credentials, "irma-demo.RU.studentCard", "irma-demo.RU.studentCard not deserialized")
assert.NotEmpty(t, Manager.credentials, "irma-demo.RU.studentCard not deserialized")
cred := Manager.credentials["irma-demo.RU.studentCard"][0]
}
func verifyStoreIsUnmarshaled(t *testing.T) {
cred, err := Manager.Credential("irma-demo.RU.studentCard", 0)
assert.NoError(t, err, "could not fetch credential")
assert.NotNil(t, cred, "Credential should exist")
assert.NotNil(t, cred.Attributes[0], "Metadata attribute of irma-demo.RU.studentCard should not be nil")
assert.True(t,
Manager.credentials["irma-demo.RU.studentCard"][0].Signature.Verify(cred.PublicKey(), cred.Attributes),
cred.Signature.Verify(cred.PublicKey(), cred.Attributes),
"Credential should be valid",
)
}
func TestAndroidParse(t *testing.T) {
parseAndroidStorage(t)
verifyStoreIsUnmarshaled(t)
}
func TestUnmarshaling(t *testing.T) {
parseAndroidStorage(t)
Manager = newCredentialManager()
Manager.Init("testdata/storage/test")
verifyStoreIsUnmarshaled(t)
}
......@@ -12,41 +12,87 @@ import (
)
// Manager is the global instance of CredentialManager.
var Manager = CredentialManager{
attributes: make(map[string][]*AttributeList),
credentials: make(map[string][]*gabi.Credential),
}
var Manager = newCredentialManager()
// CredentialManager manages credentials.
type CredentialManager struct {
secretkey *big.Int
storagePath string
attributes map[string][]*AttributeList
credentials map[string][]*gabi.Credential
credentials map[string]map[int]*gabi.Credential
}
func (cm *CredentialManager) generateSecretKey() (err error) {
cm.secretkey, err = gabi.RandomBigInt(gabi.DefaultSystemParameters[1024].Lm)
return
func newCredentialManager() *CredentialManager {
return &CredentialManager{
credentials: make(map[string]map[int]*gabi.Credential),
}
}
func (cm *CredentialManager) generateSecretKey() (sk *big.Int, err error) {
return gabi.RandomBigInt(gabi.DefaultSystemParameters[1024].Lm)
}
// Init deserializes the credentials from storage.
func (cm *CredentialManager) Init(path string) (err error) {
cm.storagePath = path
cm.ensureStorageExists()
bytes, err := ioutil.ReadFile(cm.path(skFile))
err = cm.ensureStorageExists()
if err != nil {
return err
}
cm.secretkey, err = cm.loadSecretKey()
if err != nil {
return
}
cm.secretkey = new(big.Int).SetBytes(bytes)
cm.attributes, err = cm.loadAttributes()
return
}
// Attributes returns the attribute list of the requested credential, or nil if we do not have it.
func (cm *CredentialManager) Attributes(id string, counter int) (attributes *AttributeList) {
list, exists := cm.attributes[id]
if !exists || len(list) <= counter {
return
}
return list[counter]
}
// Credential returns the requested credential, or nil if we do not have it.
func (cm *CredentialManager) Credential(id string, counter int) (cred *gabi.Credential, err error) {
_, exists := cm.credentials[id]
if !exists {
cm.credentials[id] = make(map[int]*gabi.Credential)
}
// If the requested credential is not in credential map, we check if its attributes were
// deserialized during Init(). If so, there should be a corresponding signature file,
// so we read that, construct the credential, and add it to the credential map
if _, exists := cm.credentials[id][counter]; !exists {
attrs := cm.Attributes(id, counter)
if attrs == nil { // We do not have the requested cred
return
}
ints := append([]*big.Int{cm.secretkey}, attrs.Ints...)
sig, err := cm.loadSignature(id, counter)
if err != nil {
return nil, err
}
if sig == nil {
err = errors.New("signature file not found")
return nil, err
}
cred := gabi.NewCredential(ints, sig, nil)
cm.credentials[id][counter] = cred
}
return cm.credentials[id][counter], nil
}
// ParseAndroidStorage parses an Android cardemu.xml shared preferences file
// from the old Android IRMA app, parsing its credentials into the current instance.
// 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 {
......@@ -54,27 +100,26 @@ func (cm *CredentialManager) ParseAndroidStorage() (err error) {
}
bytes, err := ioutil.ReadFile(cm.path(cardemuXML))
parsed := struct {
parsedxml := struct {
Strings []struct {
Name string `xml:"name,attr"`
Content string `xml:",chardata"`
} `xml:"string"`
}{}
xml.Unmarshal(bytes, &parsed)
xml.Unmarshal(bytes, &parsedxml)
for _, xmltag := range parsed.Strings {
parsedjson := make(map[string][]*gabi.Credential)
for _, xmltag := range parsedxml.Strings {
if xmltag.Name == "credentials" {
jsontag := html.UnescapeString(xmltag.Content)
if err = json.Unmarshal([]byte(jsontag), &cm.credentials); err != nil {
if err = json.Unmarshal([]byte(jsontag), &parsedjson); err != nil {
return
}
}
}
for _, list := range cm.credentials {
if list != nil && len(list) > 0 {
cm.secretkey = list[0].Attributes[0]
}
for _, list := range parsedjson {
cm.secretkey = list[0].Attributes[0]
for i, cred := range list {
// TODO move this metadata initialisation somehow into gabi.Credential?
cred.MetadataAttribute = gabi.MetadataFromInt(cred.Attributes[1])
......@@ -95,7 +140,7 @@ func (cm *CredentialManager) ParseAndroidStorage() (err error) {
if err != nil {
return err
}
err = cm.storeKey()
err = cm.storeSecretKey(cm.secretkey)
if err != nil {
return err
}
......@@ -112,9 +157,10 @@ func (cm *CredentialManager) addCredential(cred *gabi.Credential) {
cm.attributes[id] = append(cm.attributes[id], NewAttributeListFromInts(cred.Attributes[1:]))
if _, exists := cm.credentials[id]; !exists {
cm.credentials[id] = make([]*gabi.Credential, 0, 1)
cm.credentials[id] = make(map[int]*gabi.Credential)
}
cm.credentials[id] = append(cm.credentials[id], cred)
counter := len(cm.attributes[id]) - 1
cm.credentials[id][counter] = cred
}
// Add adds the specified credential to the CredentialManager.
......@@ -124,7 +170,7 @@ func (cm *CredentialManager) Add(cred *gabi.Credential) (err error) {
}
cm.addCredential(cred)
counter := len(cm.credentials) - 1
counter := len(cm.credentials[cred.CredentialType().Identifier()]) - 1
err = cm.storeSignature(cred, counter)
if err != nil {
......
......@@ -7,7 +7,11 @@ import (
"os"
"strconv"
"crypto/rand"
"encoding/hex"
"github.com/mhe/gabi"
"math/big"
"path"
)
// Filenames in which we store stuff
......@@ -15,7 +19,7 @@ const (
skFile = "sk"
attributesFile = "attrs"
signaturesDir = "sigs"
cardemuXML = "cardemu.xml"
cardemuXML = "../cardemu.xml"
)
func pathExists(path string) (bool, error) {
......@@ -33,6 +37,15 @@ func (cm *CredentialManager) path(file string) string {
return cm.storagePath + "/" + file
}
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 {
......@@ -42,43 +55,6 @@ func (cm *CredentialManager) ensureStorageExists() (err error) {
return errors.New("credential storage path does not exist")
}
var file *os.File
exist, err = pathExists(cm.path(skFile))
if err != nil {
return
}
if !exist {
err = cm.generateSecretKey()
if err != nil {
return
}
file, err = os.Create(cm.path(skFile))
if err != nil {
return
}
defer file.Close()
_, err = file.Write(cm.secretkey.Bytes())
if err != nil {
return
}
}
exist, err = pathExists(cm.path(attributesFile))
if err != nil {
return err
}
if !exist {
file, err = os.Create(cm.path(attributesFile))
if err != nil {
return
}
defer file.Close()
_, err = file.Write([]byte("{}"))
if err != nil {
return
}
}
exist, err = pathExists(cm.path(signaturesDir))
if err != nil {
return err
......@@ -90,8 +66,32 @@ func (cm *CredentialManager) ensureStorageExists() (err error) {
return
}
func (cm *CredentialManager) storeKey() error {
return ioutil.WriteFile(cm.path(skFile), cm.secretkey.Bytes(), 0600)
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)
}
func (cm *CredentialManager) storeSignature(cred *gabi.Credential, counter int) (err error) {
......@@ -105,7 +105,7 @@ func (cm *CredentialManager) storeSignature(cred *gabi.Credential, counter int)
}
// TODO existence check
filename := cm.path(signaturesDir) + "/" + cred.CredentialType().Identifier() + "-" + strconv.Itoa(counter)
filename := cm.signatureFilename(cred.CredentialType().Identifier(), counter)
err = ioutil.WriteFile(filename, credbytes, 0600)
return
}
......@@ -120,3 +120,57 @@ func (cm *CredentialManager) storeAttributes() (err error) {
err = ioutil.WriteFile(cm.path(attributesFile), attrbytes, 0600)
return
}
func (cm *CredentialManager) loadSignature(id string, counter int) (signature *gabi.CLSignature, err error) {
path := cm.signatureFilename(id, counter)
exists, err := pathExists(path)
if err != nil || !exists {
return
}
bytes, err := ioutil.ReadFile(path)
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))
if err != nil {
return nil, err
}
if exists {
bytes, err := ioutil.ReadFile(cm.path(skFile))
if err != nil {
return nil, err
}
return new(big.Int).SetBytes(bytes), nil
} else {
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[string][]*AttributeList, err error) {
list = make(map[string][]*AttributeList)
exists, err := pathExists(cm.path(attributesFile))
if err != nil || !exists {
return
}
bytes, err := ioutil.ReadFile(cm.path(attributesFile))
if err != nil {
return nil, err
}
return list, json.Unmarshal(bytes, &list)
}
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment