Wire-compatible with C mosh. AES-128-OCB3, SSP transport, fragmentation, protobuf state sync, latch extension fields. Includes ServeRW bridge mode for embedding.
267 lines
6.2 KiB
Go
267 lines
6.2 KiB
Go
package mosh
|
|
|
|
import (
|
|
"crypto/aes"
|
|
"crypto/cipher"
|
|
"crypto/subtle"
|
|
"encoding/base64"
|
|
"fmt"
|
|
)
|
|
|
|
const (
|
|
blockSize = 16
|
|
tagSize = 16
|
|
)
|
|
|
|
// OCB implements AES-128-OCB3 authenticated encryption (RFC 7253).
|
|
type OCB struct {
|
|
enc cipher.Block
|
|
dec cipher.Block
|
|
lStar [blockSize]byte
|
|
lDollar [blockSize]byte
|
|
l [32][blockSize]byte
|
|
}
|
|
|
|
// NewOCBFromBase64 creates an OCB cipher from a base64-encoded key.
|
|
func NewOCBFromBase64(b64 string) (*OCB, error) {
|
|
key, err := base64.StdEncoding.DecodeString(b64)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("ocb: bad base64: %w", err)
|
|
}
|
|
return NewOCB(key)
|
|
}
|
|
|
|
// NewOCB creates an OCB cipher from a 16-byte AES key.
|
|
func NewOCB(key []byte) (*OCB, error) {
|
|
if len(key) != 16 {
|
|
return nil, fmt.Errorf("ocb: key must be 16 bytes, got %d", len(key))
|
|
}
|
|
enc, err := aes.NewCipher(key)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
// AES decrypt block for OCB decrypt.
|
|
dec, err := aes.NewCipher(key)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
o := &OCB{enc: enc, dec: dec}
|
|
|
|
// L_* = ENCIPHER(K, zeros)
|
|
enc.Encrypt(o.lStar[:], make([]byte, blockSize))
|
|
|
|
// L_$ = double(L_*)
|
|
o.lDollar = gfDouble(o.lStar)
|
|
|
|
// L_0..L_31 = successive doublings
|
|
o.l[0] = gfDouble(o.lDollar)
|
|
for i := 1; i < 32; i++ {
|
|
o.l[i] = gfDouble(o.l[i-1])
|
|
}
|
|
return o, nil
|
|
}
|
|
|
|
// Encrypt encrypts plaintext with the given nonce (max 15 bytes).
|
|
// Returns ciphertext followed by tag (16 bytes), matching mosh's wire format.
|
|
func (o *OCB) Encrypt(nonce, plaintext []byte) []byte {
|
|
offset := o.initOffset(nonce)
|
|
var checksum [blockSize]byte
|
|
|
|
fullBlocks := len(plaintext) / blockSize
|
|
remaining := len(plaintext) % blockSize
|
|
ciphertext := make([]byte, len(plaintext))
|
|
|
|
for i := 0; i < fullBlocks; i++ {
|
|
xorInto(&offset, &o.l[ntz(i+1)])
|
|
|
|
// XOR plaintext block into checksum.
|
|
var pBlock [blockSize]byte
|
|
copy(pBlock[:], plaintext[i*blockSize:])
|
|
xorInto(&checksum, &pBlock)
|
|
|
|
// C_i = Offset xor ENCIPHER(K, Offset xor P_i)
|
|
var tmp [blockSize]byte
|
|
xorBlocks(&tmp, &offset, &pBlock)
|
|
var enc [blockSize]byte
|
|
o.enc.Encrypt(enc[:], tmp[:])
|
|
xorInto(&enc, &offset)
|
|
copy(ciphertext[i*blockSize:], enc[:])
|
|
}
|
|
|
|
if remaining > 0 {
|
|
xorInto(&offset, &o.lStar)
|
|
|
|
var pad [blockSize]byte
|
|
o.enc.Encrypt(pad[:], offset[:])
|
|
|
|
pStar := plaintext[fullBlocks*blockSize:]
|
|
for i := 0; i < remaining; i++ {
|
|
ciphertext[fullBlocks*blockSize+i] = pStar[i] ^ pad[i]
|
|
}
|
|
|
|
// Checksum_* = Checksum_m xor (P_* || 1 || zeros)
|
|
var padded [blockSize]byte
|
|
copy(padded[:], pStar)
|
|
padded[remaining] = 0x80
|
|
xorInto(&checksum, &padded)
|
|
}
|
|
|
|
// Tag = ENCIPHER(K, Checksum xor Offset xor L_$)
|
|
xorInto(&checksum, &offset)
|
|
xorInto(&checksum, &o.lDollar)
|
|
var tag [blockSize]byte
|
|
o.enc.Encrypt(tag[:], checksum[:])
|
|
|
|
// Return ciphertext || tag (mosh wire order).
|
|
result := make([]byte, len(ciphertext)+tagSize)
|
|
copy(result, ciphertext)
|
|
copy(result[len(ciphertext):], tag[:])
|
|
return result
|
|
}
|
|
|
|
// Decrypt decrypts ciphertext+tag with the given nonce.
|
|
// Returns plaintext or nil if authentication fails.
|
|
func (o *OCB) Decrypt(nonce, ciphertextAndTag []byte) []byte {
|
|
if len(ciphertextAndTag) < tagSize {
|
|
return nil
|
|
}
|
|
|
|
ciphertext := ciphertextAndTag[:len(ciphertextAndTag)-tagSize]
|
|
tag := ciphertextAndTag[len(ciphertextAndTag)-tagSize:]
|
|
|
|
offset := o.initOffset(nonce)
|
|
var checksum [blockSize]byte
|
|
|
|
fullBlocks := len(ciphertext) / blockSize
|
|
remaining := len(ciphertext) % blockSize
|
|
plaintext := make([]byte, len(ciphertext))
|
|
|
|
for i := 0; i < fullBlocks; i++ {
|
|
xorInto(&offset, &o.l[ntz(i+1)])
|
|
|
|
var cBlock [blockSize]byte
|
|
copy(cBlock[:], ciphertext[i*blockSize:])
|
|
var tmp [blockSize]byte
|
|
xorBlocks(&tmp, &offset, &cBlock)
|
|
var dec [blockSize]byte
|
|
o.dec.Decrypt(dec[:], tmp[:])
|
|
xorInto(&dec, &offset)
|
|
copy(plaintext[i*blockSize:], dec[:])
|
|
xorInto(&checksum, &dec)
|
|
}
|
|
|
|
if remaining > 0 {
|
|
xorInto(&offset, &o.lStar)
|
|
|
|
var pad [blockSize]byte
|
|
o.enc.Encrypt(pad[:], offset[:])
|
|
|
|
cStar := ciphertext[fullBlocks*blockSize:]
|
|
for i := 0; i < remaining; i++ {
|
|
plaintext[fullBlocks*blockSize+i] = cStar[i] ^ pad[i]
|
|
}
|
|
|
|
var padded [blockSize]byte
|
|
copy(padded[:], plaintext[fullBlocks*blockSize:fullBlocks*blockSize+remaining])
|
|
padded[remaining] = 0x80
|
|
xorInto(&checksum, &padded)
|
|
}
|
|
|
|
// Compute expected tag.
|
|
xorInto(&checksum, &offset)
|
|
xorInto(&checksum, &o.lDollar)
|
|
var expectedTag [blockSize]byte
|
|
o.enc.Encrypt(expectedTag[:], checksum[:])
|
|
|
|
// Constant-time tag comparison.
|
|
if subtle.ConstantTimeCompare(tag, expectedTag[:]) != 1 {
|
|
return nil
|
|
}
|
|
return plaintext
|
|
}
|
|
|
|
// initOffset computes the initial offset from a nonce (RFC 7253 Section 4.2).
|
|
func (o *OCB) initOffset(nonce []byte) [blockSize]byte {
|
|
nonceLen := len(nonce)
|
|
if nonceLen > 15 {
|
|
panic("ocb: nonce must be <= 15 bytes")
|
|
}
|
|
|
|
// Build the full 16-byte nonce block.
|
|
var nn [blockSize]byte
|
|
nn[0] = byte(((tagSize * 8) % 128) & 0x7f)
|
|
nn[blockSize-1-nonceLen] |= 0x01
|
|
copy(nn[blockSize-nonceLen:], nonce)
|
|
|
|
bottom := nn[15] & 0x3f
|
|
nn[15] &= 0xc0
|
|
|
|
// Ktop = ENCIPHER(K, nn)
|
|
var ktop [blockSize]byte
|
|
o.enc.Encrypt(ktop[:], nn[:])
|
|
|
|
// Stretch = Ktop || (Ktop[0..7] xor Ktop[1..8])
|
|
var stretch [24]byte
|
|
copy(stretch[:16], ktop[:])
|
|
for i := 0; i < 8; i++ {
|
|
stretch[16+i] = ktop[i] ^ ktop[i+1]
|
|
}
|
|
|
|
// Extract 128 bits starting at bit position `bottom`.
|
|
var offset [blockSize]byte
|
|
byteShift := int(bottom) >> 3
|
|
bitShift := int(bottom) & 7
|
|
|
|
for i := 0; i < blockSize; i++ {
|
|
idx := byteShift + i
|
|
if idx < 24 {
|
|
offset[i] = (stretch[idx] << bitShift) & 0xff
|
|
if idx+1 < 24 {
|
|
offset[i] |= stretch[idx+1] >> (8 - bitShift)
|
|
}
|
|
}
|
|
}
|
|
return offset
|
|
}
|
|
|
|
// gfDouble doubles a block in GF(2^128) with polynomial x^128+x^7+x^2+x+1.
|
|
func gfDouble(block [blockSize]byte) [blockSize]byte {
|
|
var result [blockSize]byte
|
|
carry := byte(0)
|
|
for i := blockSize - 1; i >= 0; i-- {
|
|
tmp := (block[i] << 1) | carry
|
|
result[i] = tmp
|
|
carry = block[i] >> 7
|
|
}
|
|
if carry != 0 {
|
|
result[blockSize-1] ^= 0x87
|
|
}
|
|
return result
|
|
}
|
|
|
|
// ntz returns the number of trailing zeros of a positive integer.
|
|
func ntz(n int) int {
|
|
if n == 0 {
|
|
return 32
|
|
}
|
|
count := 0
|
|
for (n & 1) == 0 {
|
|
count++
|
|
n >>= 1
|
|
}
|
|
return count
|
|
}
|
|
|
|
func xorInto(a *[blockSize]byte, b *[blockSize]byte) {
|
|
for i := 0; i < blockSize; i++ {
|
|
a[i] ^= b[i]
|
|
}
|
|
}
|
|
|
|
func xorBlocks(dst, a, b *[blockSize]byte) {
|
|
for i := 0; i < blockSize; i++ {
|
|
dst[i] = a[i] ^ b[i]
|
|
}
|
|
}
|