← Todos os textos

Partilhar dados cifrados em meios de formato curto

Nesta página

Partilhar grandes quantidades de dados em meios de formato curto, como códigos QR ou etiquetas NFC, pode ser difícil. Os dados têm de ser cifrados para garantir a confidencialidade, mas meter dados úteis num espaço pequeno dá trabalho.

Eu procurava uma forma de partilhar dados em segurança, num meio de formato curto, com um destinatário em quem não tenho confiança prévia.

Enquanto trabalhava nisto, inspirei-me no Signal Protocol e na Noise Protocol Framework.

Os dois protocolos combinam cifragem simétrica e assimétrica para chegar a um nível alto de segurança e, ao mesmo tempo, reduzir ao mínimo os dados que é preciso transferir.

Eu queria chegar a algo parecido, mas sem depender de cifragem assimétrica. Queria usar uma chave simétrica para cifrar os dados e partilhar essa chave de par para par, no meio de formato curto. Queria também usar um intermediário centralizado para guardar os dados cifrados e fornecer um token.

Em alguns aspetos, lembra a computação multipartidária segura.

Cheguei a um protocolo a que chamo Tokenized Encrypted Payload Sharing (TEPS). Documentei-o abaixo. Não tenho a certeza da minha abordagem, por isso agradecia mesmo qualquer comentário.

Tokenized Encrypted Payload Sharing (TEPS)

Esta é uma técnica para cifrar dados que ficam guardados temporariamente num intermediário. O intermediário não tem a confiança de ver os dados, mas tem a de os transformar em token. O cliente cifra os dados com uma chave simétrica e o token serve para ir buscar os dados ao intermediário. O cliente pode depois decifrar os dados com a mesma chave simétrica. A técnica é útil para partilhar grandes quantidades de dados em meios de formato curto, como códigos QR ou etiquetas NFC.

Um exemplo é um telemóvel a partilhar uma grande quantidade de dados com outro telemóvel. Quem envia cifra os dados e manda-os a um intermediário. O intermediário guarda os dados cifrados e devolve um token. Quem envia partilha então o token e a chave simétrica com o destinatário, através de um meio de formato curto, como um código QR ou uma etiqueta NFC. O destinatário usa o token para ir buscar os dados cifrados ao intermediário. Depois pode decifrar os dados com a mesma chave simétrica, ou partilhar o token e a chave simétrica com outra parte para ela também os poder obter.

Especificação

1. Introdução

O que se segue é a especificação do protocolo TEPS. O protocolo descreve o processo de partilhar dados cifrados através de um intermediário e de partilhar o token e a chave simétrica de par para par.

2. Terminologia

  • Intermediário: Um servidor centralizado de confiança que guarda os dados e fornece um token.
  • Chave de cifragem simétrica: Uma chave simétrica aleatória usada para cifrar os dados.
  • Token: Um ID único usado para ir buscar os dados ao intermediário.
  • Cliente: A entidade que cifra os dados e os envia ao intermediário. (ex.: um telemóvel)
  • Meio de partilha: O meio usado para transferir o token e a chave de cifragem simétrica entre clientes. (ex.: código QR, etiqueta NFC, etc.)

3. Protocolo

3.1 Visão geral

O cliente gera uma chave simétrica aleatória e cifra os dados com ela. Depois envia os dados cifrados ao intermediário em troca de um token. O intermediário guarda os dados cifrados e o token, e devolve o token ao cliente. O cliente combina então o token e a chave simétrica e partilha o resultado com o cliente destinatário através do meio de partilha (ex.: código QR, etiqueta NFC, etc.). O destinatário usa o token para ir buscar os dados cifrados ao intermediário. O cliente destinatário decifra então os dados com a chave simétrica.

Nalguns casos, o destinatário pode passar a informação a outra parte, mas isso não está coberto por esta especificação.

3.2 Processo

3.2.1 Gerar a chave simétrica

O cliente gera uma chave simétrica aleatória. A chave simétrica serve para cifrar os dados. Mais tarde, será combinada com o token do intermediário para ser partilhada de par para par. Pode ter qualquer comprimento, mas recomenda-se um tamanho adequado ao meio de partilha.

// Generate a 32 byte symmetric key
symmetricKey := make([]byte, 32)

if _, err := rand.Read(symmetricKey); err != nil {
  panic(err)
}

3.2.2 Cifrar os dados

O cliente cifra os dados com a chave simétrica, usando o algoritmo AES-256 (ou semelhante).

Por exemplo:

// Encrypt the data using the symmetric key
block, err := aes.NewCipher(symmetricKey)

if err != nil {
  panic(err)
}

gcm, err := cipher.NewGCM(block)

if err != nil {
  panic(err)
}

nonce := make([]byte, gcm.NonceSize())

if _, err = rand.Read(nonce); err != nil {
  panic(err)
}

ciphertext := gcm.Seal(nonce, nonce, data, nil)

3.2.3 Enviar os dados ao intermediário

O cliente envia os dados cifrados ao intermediário em troca de um token.

Por exemplo:

POST /data HTTP/1.1
Host: api.example.com
Content-Type: application/json
Content-Length: 123

{
  "data": "encrypted data"
}

Em código:

// Submit the encrypted data to the intermediary
req, err := http.NewRequest("POST", "https://api.example.com/data", bytes.NewBuffer(ciphertext))

if err != nil {
  panic(err)
}

req.Header.Set("Content-Type", "application/json")

resp, err := http.DefaultClient.Do(req)

if err != nil {
  panic(err)
}

defer resp.Body.Close()

body, err := ioutil.ReadAll(resp.Body)

if err != nil {
  panic(err)
}

var data map[string]interface{}

if err := json.Unmarshal(body, &data); err != nil {
  panic(err)
}

lookupID := data["lookup_id"].(string)

3.2.4 Guardar os dados e gerar o token

O intermediário guarda os dados cifrados e gera um token aleatório. O token serve para ir buscar os dados ao intermediário. Pode ter qualquer comprimento, mas recomenda-se um tamanho adequado ao meio de partilha.

O intermediário devolve então o token ao cliente.

Por exemplo:

HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 123

{
  "lookup_id": "random token"
}

Em código:

// Store the encrypted data and generate a token
lookupID := uuid.New().String()

if err := storeData(lookupID, ciphertext); err != nil {
  panic(err)
}

// Return the token to the client
resp, err := json.Marshal(map[string]interface{}{
  "lookup_id": lookupID,
})

if err != nil {
  panic(err)
}

w.Header().Set("Content-Type", "application/json")
w.Write(resp)

3.2.5 Expirar os dados (opcional)

O intermediário pode, se quiser, expirar os dados passado algum tempo. Pode usar qualquer método para isso.

Guarda a data de expiração junto com os dados:

{
  "data": "encrypted data",
  "expiry_date": "2023-07-19T21:12:43+10:00"
}

Usa um cron job para apagar os dados expirados.

3.2.6 Combinar o token e a chave simétrica

O cliente combina o token e a chave simétrica. Pode escolher como empacotar o conjunto, desde que o método seja compatível com o meio de partilha e com o cliente destinatário.

Para efeitos desta especificação, o token e a chave simétrica combinam-se no seguinte formato:

<lookup_id><symmetric_key>

3.2.7 Partilhar o token e a chave simétrica

O cliente partilha o token e a chave simétrica com o cliente destinatário através do meio de partilha (ex.: código QR, etiqueta NFC, etc.).

3.2.8 Obter os dados do intermediário

O cliente destinatário vai buscar os dados cifrados ao intermediário com o token.

Por exemplo:

GET /data/<lookup_id> HTTP/1.1
Host: api.example.com
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 123

{
  "data": "encrypted data"
}

Em código:

// Retrieve the encrypted data from the intermediary
resp, err := http.Get(fmt.Sprintf("https://api.example.com/data/%s", lookupID))

if err != nil {
  panic(err)
}

defer resp.Body.Close()

body, err := ioutil.ReadAll(resp.Body)

if err != nil {
  panic(err)
}

var data map[string]interface{}

if err := json.Unmarshal(body, &data); err != nil {
  panic(err)
}

ciphertext := data["data"].(string)

3.2.9 Decifrar os dados

O cliente destinatário decifra os dados com a chave simétrica.

Por exemplo:

// Decrypt the data using the symmetric key
block, err := aes.NewCipher(symmetricKey)

if err != nil {
  panic(err)
}

gcm, err := cipher.NewGCM(block)

if err != nil {
  panic(err)
}

nonceSize := gcm.NonceSize()

nonce, ciphertext := ciphertext[:nonceSize], ciphertext[nonceSize:]

plaintext, err := gcm.Open(nil, nonce, ciphertext, nil)

if err != nil {
  panic(err)
}

Pedido de comentários

Agradecia qualquer comentário sobre este protocolo. Se tiveres algum, fala comigo.

Obrigado por leres!