Skip to content

Instantly share code, notes, and snippets.

@piotrkubisa
Created January 23, 2020 18:03
Show Gist options
  • Save piotrkubisa/e6e5c4831f31af9301c9bc8efd7044e6 to your computer and use it in GitHub Desktop.
Save piotrkubisa/e6e5c4831f31af9301c9bc8efd7044e6 to your computer and use it in GitHub Desktop.
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