Created
January 23, 2020 18:03
-
-
Save piotrkubisa/e6e5c4831f31af9301c9bc8efd7044e6 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package model | |
import ( | |
"bytes" | |
"crypto/aes" | |
"crypto/cipher" | |
"crypto/hmac" | |
"crypto/rand" | |
"crypto/sha256" | |
"encoding/base64" | |
"encoding/hex" | |
"encoding/json" | |
"errors" | |
"io" | |
) | |
// BSO stands for Basic Storage Object | |
// | |
// A Basic Storage Object (BSO) is the generic JSON wrapper around all items | |
// passed into and out of the SyncStorage server. Like all JSON documents, BSOs | |
// are composed of unicode character data rather than raw bytes and must be | |
// encoded for transmission over the network. The SyncStorage service always | |
// encodes BSOs in UTF8. | |
// | |
// Example: | |
// { | |
// "id": "-F_Szdjg3GzX", | |
// "modified": 1388635807.41, | |
// "sortindex": 140, | |
// "payload": "{ \"this is\": \"an example\" }" | |
// } | |
type BSO struct { | |
// An identifying string. For a user, the id must be unique for a BSO | |
// within a collection, though objects in different collections may have | |
// the same ID. | |
// | |
// BSO ids must only contain printable ASCII characters. They should be | |
// exactly 12 base64-urlsafe characters; while this isn’t enforced by the | |
// server, the Firefox client expects it in most cases. | |
// | |
// Default=required | |
// Type/Max=string, 64 | |
ID string `json:"id"` | |
// The timestamp at which this object was last modified, in seconds since | |
// UNIX epoch (1970-01-01 00:00:00 UTC). This is set automatically by the | |
// server according to its own clock; any client-supplied value for this | |
// field is ignored. | |
// | |
// Default=none | |
// Type/Max=float, 2 decimal places | |
Modified float64 `json:"modified"` | |
// An integer indicating the relative importance of this item in the | |
// collection. | |
// | |
// Default=none | |
// Type/Max=integer, 9 digits | |
SortIndex int `json:"sortindex"` | |
// A string containing the data of the record. The structure of this string | |
// is defined separately for each BSO type. This spec makes no requirements | |
// for its format. In practice, JSONObjects are common. | |
// | |
// Servers must support payloads up to 256KiB in size. They may accept | |
// larger payloads, and advertise their maximum payload size via dynamic | |
// configuration. | |
// | |
// Default=empty string | |
// Type/Max=string, at least 256KiB | |
Payload string `json:"payload"` | |
// The number of seconds to keep this record. After that time this item | |
// will no longer be returned in response to any request, and it may be | |
// pruned from the database. If not specified or null, the record will not | |
// expire. | |
// | |
// This field may be set on write, but is not returned by the server. | |
// | |
// Default=none | |
// Type/Max=integer, positive, 9 digits | |
TTL int64 `json:"ttl,omitempty"` | |
} | |
// DecryptPayload decrypt the payload of the BSO. | |
// | |
// To decrypt payload is required encryptionKey and hmacKey pair, which can be | |
// obtained by requesting them from https://api.accounts.firefox.com/v1 with | |
// provided username and password. When master key is fetched, it is recommended | |
// to decrypt record from "crypto" collection with id: "keys" it will contains | |
// encryptionKey which can be further used to do decryption. | |
// | |
// See: https://github.com/mozilla-services/syncclient/issues/30#issuecomment-280517782 | |
func (bso *BSO) DecryptPayload(encryptionKey, hmacKey []byte) ([]byte, error) { | |
bp, err := bso.decodePayload() | |
if err != nil { | |
return nil, err | |
} | |
pt, err := bp.decrypt(encryptionKey, hmacKey) | |
if err != nil { | |
return nil, err | |
} | |
return pt, nil | |
} | |
// DecryptPayloadAs decrypts payload and unmarshals it | |
func (bso *BSO) DecryptPayloadAs(encryptionKey, hmacKey []byte, target interface{}) error { | |
pt, err := bso.DecryptPayload(encryptionKey, hmacKey) | |
if err != nil { | |
return err | |
} | |
return json.Unmarshal(pt, target) | |
} | |
type bsoPayload struct { | |
CipherText string `json:"ciphertext"` | |
Hmac string `json:"hmac"` | |
IV string `json:"IV"` | |
} | |
func (bso *BSO) decodePayload() (bsoPayloadDecoded, error) { | |
var bp bsoPayloadDecoded | |
var err error | |
var jd bsoPayload | |
if err = json.Unmarshal([]byte(bso.Payload), &jd); err != nil { | |
return bp, err | |
} | |
bp.CipherTextRaw = []byte(jd.CipherText) | |
bp.CipherText, err = base64.StdEncoding.DecodeString(jd.CipherText) | |
if err != nil { | |
return bp, err | |
} | |
bp.Hmac, err = hex.DecodeString(jd.Hmac) | |
if err != nil { | |
return bp, err | |
} | |
bp.IV, err = base64.StdEncoding.DecodeString(jd.IV) | |
if err != nil { | |
return bp, err | |
} | |
return bp, nil | |
} | |
type bsoPayloadDecoded struct { | |
CipherTextRaw []byte | |
CipherText []byte | |
Hmac []byte | |
IV []byte | |
} | |
func (bp *bsoPayloadDecoded) decrypt(encryptionKey, hmacKey []byte) ([]byte, error) { | |
expectedHmac := hmac.New(sha256.New, hmacKey) | |
if _, err := expectedHmac.Write(bp.CipherTextRaw); err != nil { | |
return nil, err | |
} | |
if !hmac.Equal(expectedHmac.Sum(nil), bp.Hmac) { | |
return nil, errors.New("HMAC mismatch") | |
} | |
plaintext, err := aesCBCDecrypt(encryptionKey, bp.CipherText, bp.IV) | |
if err != nil { | |
return nil, err | |
} | |
return pkcs7UnPadding(plaintext) | |
} | |
func aesCBCDecrypt(encryptionKey, cipherText, iv []byte) ([]byte, error) { | |
block, err := aes.NewCipher(encryptionKey) | |
if err != nil { | |
return nil, err | |
} | |
if len(cipherText) < aes.BlockSize { | |
return nil, errors.New("Invalid ciphertext. It must be a multiple of the block size") | |
} | |
if len(cipherText)%aes.BlockSize != 0 { | |
return nil, errors.New("Invalid ciphertext. It must be a multiple of the block size") | |
} | |
plaintext := make([]byte, len(cipherText)) | |
cipher. | |
NewCBCDecrypter(block, iv[:block.BlockSize()]). | |
CryptBlocks(plaintext, cipherText) | |
return plaintext, nil | |
} | |
func pkcs7UnPadding(src []byte) ([]byte, error) { | |
length := len(src) | |
unpadding := int(src[length-1]) | |
if unpadding > aes.BlockSize || unpadding == 0 { | |
return nil, errors.New("Invalid pkcs7 padding (unpadding > aes.BlockSize || unpadding == 0)") | |
} | |
pad := src[len(src)-unpadding:] | |
for i := 0; i < unpadding; i++ { | |
if pad[i] != byte(unpadding) { | |
return nil, errors.New("Invalid pkcs7 padding (pad[i] != unpadding)") | |
} | |
} | |
return src[:(length - unpadding)], nil | |
} | |
// EncryptPayload encrypts the payload for the BSO. | |
// | |
// During encryption there are used encryptionKey and hmacKey (raw bytes), which | |
// can be obtained from decrypting crypto/keys BSO, optionally a user-specific | |
// master encryption key with hmac key requested from identity server. | |
func (bso *BSO) EncryptPayload(encryptionKey, hmacKey []byte, payload interface{}) error { | |
payloadRaw, err := json.Marshal(payload) | |
if err != nil { | |
return err | |
} | |
cipherText, iv, err := aesCBCEncrypt(encryptionKey, pkcs7Padding(payloadRaw)) | |
if err != nil { | |
return err | |
} | |
cipherTextB64 := base64.StdEncoding.EncodeToString(cipherText) | |
m := hmac.New(sha256.New, hmacKey) | |
if _, err := m.Write([]byte(cipherTextB64)); err != nil { | |
return err | |
} | |
bp := bsoPayload{ | |
Hmac: hex.EncodeToString(m.Sum(nil)), | |
CipherText: cipherTextB64, | |
IV: base64.StdEncoding.EncodeToString(iv), | |
} | |
payloadEncrypted, err := json.Marshal(bp) | |
if err != nil { | |
return err | |
} | |
bso.Payload = string(payloadEncrypted) | |
return nil | |
} | |
func aesCBCEncrypt(key, s []byte) ([]byte, []byte, error) { | |
return aesCBCEncryptWithRand(rand.Reader, key, s) | |
} | |
func aesCBCEncryptWithRand(prng io.Reader, key, s []byte) ([]byte, []byte, error) { | |
if len(s)%aes.BlockSize != 0 { | |
return nil, nil, errors.New("Invalid plaintext. It must be a multiple of the block size") | |
} | |
block, err := aes.NewCipher(key) | |
if err != nil { | |
return nil, nil, err | |
} | |
ciphertext := make([]byte, len(s)) | |
iv := make([]byte, aes.BlockSize) | |
if _, err := io.ReadFull(prng, iv); err != nil { | |
return nil, nil, err | |
} | |
cipher. | |
NewCBCEncrypter(block, iv). | |
CryptBlocks(ciphertext[:], s) | |
return ciphertext, iv, nil | |
} | |
func pkcs7Padding(src []byte) []byte { | |
padding := aes.BlockSize - len(src)%aes.BlockSize | |
padtext := bytes.Repeat([]byte{byte(padding)}, padding) | |
return append(src, padtext...) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment