diff --git a/crypter-ffi/build-kotlin.sh b/crypter-ffi/build-kotlin.sh index 17f3a01..f342d91 100755 --- a/crypter-ffi/build-kotlin.sh +++ b/crypter-ffi/build-kotlin.sh @@ -1,8 +1,11 @@ +echo "=> creating C FFI scaffolding" +uniffi-bindgen scaffolding src/crypter.udl + echo "=> creating kotlin bindings" uniffi-bindgen generate src/crypter.udl --language kotlin -echo "=> creating C FFI scaffolding" -uniffi-bindgen scaffolding src/crypter.udl +echo "=> renaming uniffi_crypter to crypter" +sed -i '' 's/return "uniffi_crypter"/return "crypter"/' src/uniffi/crypter/crypter.kt echo "=> building i686-linux-android" cross build --target i686-linux-android --release @@ -32,8 +35,4 @@ mv target/x86_64-linux-android/release/libcrypter.so target/out/x86_64/libcrypte zip -r target/kotlin-libraries.zip target/out -echo "=> renaming uniffi_crypter to crypter" - -sed -i '' 's/return "uniffi_crypter"/return "crypter"/' src/uniffi/crypter/crypter.kt - echo "=> done!" diff --git a/crypter-ffi/build-swift.sh b/crypter-ffi/build-swift.sh new file mode 100755 index 0000000..3a4f653 --- /dev/null +++ b/crypter-ffi/build-swift.sh @@ -0,0 +1,18 @@ +echo "=> creating C FFI scaffolding" +uniffi-bindgen scaffolding src/crypter.udl + +echo "=> creating swift bindings" +uniffi-bindgen generate src/crypter.udl --language swift + +echo "=> creating swift bindings" +sed -i '' 's/module\ crypterFFI/framework\ module\ crypterFFI/' src/crypterFFI.modulemap + +echo "=> building x86_64-apple-ios" +cross build --target=x86_64-apple-ios --release +echo "=> building aarch64-apple-ios" +cross build --target=aarch64-apple-ios --release + +echo "=> combining into a universal lib" +lipo -create target/x86_64-apple-ios/release/libcrypter.a target/aarch64-apple-ios/release/libcrypter.a -output target/universal-crypter.a + +echo "=> done!" diff --git a/crypter-ffi/readme.md b/crypter-ffi/readme.md index 54b2235..fa4c784 100644 --- a/crypter-ffi/readme.md +++ b/crypter-ffi/readme.md @@ -10,15 +10,14 @@ uniffi-bindgen scaffolding src/crypter.udl rustup target add aarch64-linux-android armv7-linux-androideabi i686-linux-android x86_64-linux-android arm-linux-androideabi -uniffi-bindgen generate src/crypter.udl --language kotlin - -cross build --target i686-linux-android --release -cross build --target aarch64-linux-android --release -cross build --target arm-linux-androideabi --release -cross build --target armv7-linux-androideabi --release -cross build --target x86_64-linux-android --release +./build-kotlin.sh ### swift -uniffi-bindgen generate src/crypter.udl --language swift +rustup target add aarch64-apple-ios x86_64-apple-ios +armv7-apple-ios +armv7s-apple-ios +i386-apple-ios + +./build-swift.sh diff --git a/crypter-ffi/src/crypter.swift b/crypter-ffi/src/crypter.swift new file mode 100644 index 0000000..b738b77 --- /dev/null +++ b/crypter-ffi/src/crypter.swift @@ -0,0 +1,488 @@ +// This file was autogenerated by some hot garbage in the `uniffi` crate. +// Trust me, you don't want to mess with it! +import Foundation + +// Depending on the consumer's build setup, the low-level FFI code +// might be in a separate module, or it might be compiled inline into +// this module. This is a bit of light hackery to work with both. +#if canImport(crypterFFI) +import crypterFFI +#endif + +fileprivate extension RustBuffer { + // Allocate a new buffer, copying the contents of a `UInt8` array. + init(bytes: [UInt8]) { + let rbuf = bytes.withUnsafeBufferPointer { ptr in + RustBuffer.from(ptr) + } + self.init(capacity: rbuf.capacity, len: rbuf.len, data: rbuf.data) + } + + static func from(_ ptr: UnsafeBufferPointer) -> RustBuffer { + try! rustCall { ffi_crypter_b428_rustbuffer_from_bytes(ForeignBytes(bufferPointer: ptr), $0) } + } + + // Frees the buffer in place. + // The buffer must not be used after this is called. + func deallocate() { + try! rustCall { ffi_crypter_b428_rustbuffer_free(self, $0) } + } +} + +fileprivate extension ForeignBytes { + init(bufferPointer: UnsafeBufferPointer) { + self.init(len: Int32(bufferPointer.count), data: bufferPointer.baseAddress) + } +} + +// For every type used in the interface, we provide helper methods for conveniently +// lifting and lowering that type from C-compatible data, and for reading and writing +// values of that type in a buffer. + +// Helper classes/extensions that don't change. +// Someday, this will be in a libray of its own. + +fileprivate extension Data { + init(rustBuffer: RustBuffer) { + // TODO: This copies the buffer. Can we read directly from a + // Rust buffer? + self.init(bytes: rustBuffer.data!, count: Int(rustBuffer.len)) + } +} + +// A helper class to read values out of a byte buffer. +fileprivate class Reader { + let data: Data + var offset: Data.Index + + init(data: Data) { + self.data = data + self.offset = 0 + } + + // Reads an integer at the current offset, in big-endian order, and advances + // the offset on success. Throws if reading the integer would move the + // offset past the end of the buffer. + func readInt() throws -> T { + let range = offset...size + guard data.count >= range.upperBound else { + throw UniffiInternalError.bufferOverflow + } + if T.self == UInt8.self { + let value = data[offset] + offset += 1 + return value as! T + } + var value: T = 0 + let _ = withUnsafeMutableBytes(of: &value, { data.copyBytes(to: $0, from: range)}) + offset = range.upperBound + return value.bigEndian + } + + // Reads an arbitrary number of bytes, to be used to read + // raw bytes, this is useful when lifting strings + func readBytes(count: Int) throws -> Array { + let range = offset..<(offset+count) + guard data.count >= range.upperBound else { + throw UniffiInternalError.bufferOverflow + } + var value = [UInt8](repeating: 0, count: count) + value.withUnsafeMutableBufferPointer({ buffer in + data.copyBytes(to: buffer, from: range) + }) + offset = range.upperBound + return value + } + + // Reads a float at the current offset. + @inlinable + func readFloat() throws -> Float { + return Float(bitPattern: try readInt()) + } + + // Reads a float at the current offset. + @inlinable + func readDouble() throws -> Double { + return Double(bitPattern: try readInt()) + } + + // Indicates if the offset has reached the end of the buffer. + @inlinable + func hasRemaining() -> Bool { + return offset < data.count + } +} + +// A helper class to write values into a byte buffer. +fileprivate class Writer { + var bytes: [UInt8] + var offset: Array.Index + + init() { + self.bytes = [] + self.offset = 0 + } + + func writeBytes(_ byteArr: S) where S: Sequence, S.Element == UInt8 { + bytes.append(contentsOf: byteArr) + } + + // Writes an integer in big-endian order. + // + // Warning: make sure what you are trying to write + // is in the correct type! + func writeInt(_ value: T) { + var value = value.bigEndian + withUnsafeBytes(of: &value) { bytes.append(contentsOf: $0) } + } + + @inlinable + func writeFloat(_ value: Float) { + writeInt(value.bitPattern) + } + + @inlinable + func writeDouble(_ value: Double) { + writeInt(value.bitPattern) + } +} + +// Protocol for types that transfer other types across the FFI. This is +// analogous go the Rust trait of the same name. +fileprivate protocol FfiConverter { + associatedtype FfiType + associatedtype SwiftType + + static func lift(_ value: FfiType) throws -> SwiftType + static func lower(_ value: SwiftType) -> FfiType + static func read(from buf: Reader) throws -> SwiftType + static func write(_ value: SwiftType, into buf: Writer) +} + +// Types conforming to `Primitive` pass themselves directly over the FFI. +fileprivate protocol FfiConverterPrimitive: FfiConverter where FfiType == SwiftType { } + +extension FfiConverterPrimitive { + static func lift(_ value: FfiType) throws -> SwiftType { + return value + } + + static func lower(_ value: SwiftType) -> FfiType { + return value + } +} + +// Types conforming to `FfiConverterRustBuffer` lift and lower into a `RustBuffer`. +// Used for complex types where it's hard to write a custom lift/lower. +fileprivate protocol FfiConverterRustBuffer: FfiConverter where FfiType == RustBuffer {} + +extension FfiConverterRustBuffer { + static func lift(_ buf: RustBuffer) throws -> SwiftType { + let reader = Reader(data: Data(rustBuffer: buf)) + let value = try read(from: reader) + if reader.hasRemaining() { + throw UniffiInternalError.incompleteData + } + buf.deallocate() + return value + } + + static func lower(_ value: SwiftType) -> RustBuffer { + let writer = Writer() + write(value, into: writer) + return RustBuffer(bytes: writer.bytes) + } +} +// An error type for FFI errors. These errors occur at the UniFFI level, not +// the library level. +fileprivate enum UniffiInternalError: LocalizedError { + case bufferOverflow + case incompleteData + case unexpectedOptionalTag + case unexpectedEnumCase + case unexpectedNullPointer + case unexpectedRustCallStatusCode + case unexpectedRustCallError + case unexpectedStaleHandle + case rustPanic(_ message: String) + + public var errorDescription: String? { + switch self { + case .bufferOverflow: return "Reading the requested value would read past the end of the buffer" + case .incompleteData: return "The buffer still has data after lifting its containing value" + case .unexpectedOptionalTag: return "Unexpected optional tag; should be 0 or 1" + case .unexpectedEnumCase: return "Raw enum value doesn't match any cases" + case .unexpectedNullPointer: return "Raw pointer value was null" + case .unexpectedRustCallStatusCode: return "Unexpected RustCallStatus code" + case .unexpectedRustCallError: return "CALL_ERROR but no errorClass specified" + case .unexpectedStaleHandle: return "The object in the handle map has been dropped already" + case let .rustPanic(message): return message + } + } +} + +fileprivate let CALL_SUCCESS: Int8 = 0 +fileprivate let CALL_ERROR: Int8 = 1 +fileprivate let CALL_PANIC: Int8 = 2 + +fileprivate extension RustCallStatus { + init() { + self.init( + code: CALL_SUCCESS, + errorBuf: RustBuffer.init( + capacity: 0, + len: 0, + data: nil + ) + ) + } +} + +private func rustCall(_ callback: (UnsafeMutablePointer) -> T) throws -> T { + try makeRustCall(callback, errorHandler: { + $0.deallocate() + return UniffiInternalError.unexpectedRustCallError + }) +} + +private func rustCallWithError + (_ errorFfiConverter: F.Type, _ callback: (UnsafeMutablePointer) -> T) throws -> T + where F.SwiftType: Error, F.FfiType == RustBuffer + { + try makeRustCall(callback, errorHandler: { return try errorFfiConverter.lift($0) }) +} + +private func makeRustCall(_ callback: (UnsafeMutablePointer) -> T, errorHandler: (RustBuffer) throws -> Error) throws -> T { + var callStatus = RustCallStatus.init() + let returnedVal = callback(&callStatus) + switch callStatus.code { + case CALL_SUCCESS: + return returnedVal + + case CALL_ERROR: + throw try errorHandler(callStatus.errorBuf) + + case CALL_PANIC: + // When the rust code sees a panic, it tries to construct a RustBuffer + // with the message. But if that code panics, then it just sends back + // an empty buffer. + if callStatus.errorBuf.len > 0 { + throw UniffiInternalError.rustPanic(try FfiConverterString.lift(callStatus.errorBuf)) + } else { + callStatus.errorBuf.deallocate() + throw UniffiInternalError.rustPanic("Rust panic") + } + + default: + throw UniffiInternalError.unexpectedRustCallStatusCode + } +} + +// Public interface members begin here. + + +fileprivate struct FfiConverterString: FfiConverter { + typealias SwiftType = String + typealias FfiType = RustBuffer + + static func lift(_ value: RustBuffer) throws -> String { + defer { + value.deallocate() + } + if value.data == nil { + return String() + } + let bytes = UnsafeBufferPointer(start: value.data!, count: Int(value.len)) + return String(bytes: bytes, encoding: String.Encoding.utf8)! + } + + static func lower(_ value: String) -> RustBuffer { + return value.utf8CString.withUnsafeBufferPointer { ptr in + // The swift string gives us int8_t, we want uint8_t. + ptr.withMemoryRebound(to: UInt8.self) { ptr in + // The swift string gives us a trailing null byte, we don't want it. + let buf = UnsafeBufferPointer(rebasing: ptr.prefix(upTo: ptr.count - 1)) + return RustBuffer.from(buf) + } + } + } + + static func read(from buf: Reader) throws -> String { + let len: Int32 = try buf.readInt() + return String(bytes: try buf.readBytes(count: Int(len)), encoding: String.Encoding.utf8)! + } + + static func write(_ value: String, into buf: Writer) { + let len = Int32(value.utf8.count) + buf.writeInt(len) + buf.writeBytes(value.utf8) + } +} + + +public enum CrypterError { + + + + // Simple error enums only carry a message + case DeriveSharedSecret(message: String) + + // Simple error enums only carry a message + case Encrypt(message: String) + + // Simple error enums only carry a message + case Decrypt(message: String) + + // Simple error enums only carry a message + case BadPubkey(message: String) + + // Simple error enums only carry a message + case BadSecret(message: String) + + // Simple error enums only carry a message + case BadNonce(message: String) + + // Simple error enums only carry a message + case BadCiper(message: String) + +} + +fileprivate struct FfiConverterTypeCrypterError: FfiConverterRustBuffer { + typealias SwiftType = CrypterError + + static func read(from buf: Reader) throws -> CrypterError { + let variant: Int32 = try buf.readInt() + switch variant { + + + + + case 1: return .DeriveSharedSecret( + message: try FfiConverterString.read(from: buf) + ) + + case 2: return .Encrypt( + message: try FfiConverterString.read(from: buf) + ) + + case 3: return .Decrypt( + message: try FfiConverterString.read(from: buf) + ) + + case 4: return .BadPubkey( + message: try FfiConverterString.read(from: buf) + ) + + case 5: return .BadSecret( + message: try FfiConverterString.read(from: buf) + ) + + case 6: return .BadNonce( + message: try FfiConverterString.read(from: buf) + ) + + case 7: return .BadCiper( + message: try FfiConverterString.read(from: buf) + ) + + + default: throw UniffiInternalError.unexpectedEnumCase + } + } + + static func write(_ value: CrypterError, into buf: Writer) { + switch value { + + + + + case let .DeriveSharedSecret(message): + buf.writeInt(Int32(1)) + FfiConverterString.write(message, into: buf) + case let .Encrypt(message): + buf.writeInt(Int32(2)) + FfiConverterString.write(message, into: buf) + case let .Decrypt(message): + buf.writeInt(Int32(3)) + FfiConverterString.write(message, into: buf) + case let .BadPubkey(message): + buf.writeInt(Int32(4)) + FfiConverterString.write(message, into: buf) + case let .BadSecret(message): + buf.writeInt(Int32(5)) + FfiConverterString.write(message, into: buf) + case let .BadNonce(message): + buf.writeInt(Int32(6)) + FfiConverterString.write(message, into: buf) + case let .BadCiper(message): + buf.writeInt(Int32(7)) + FfiConverterString.write(message, into: buf) + + + } + } +} + + +extension CrypterError: Equatable, Hashable {} + +extension CrypterError: Error { } + +public func deriveSharedSecret(theirPubkey: String, mySecretKey: String) throws -> String { + return try FfiConverterString.lift( + try + + rustCallWithError(FfiConverterTypeCrypterError.self) { + + crypter_b428_derive_shared_secret( + FfiConverterString.lower(theirPubkey), + FfiConverterString.lower(mySecretKey), $0) +} + ) +} + + + +public func encrypt(plaintext: String, secret: String, nonce: String) throws -> String { + return try FfiConverterString.lift( + try + + rustCallWithError(FfiConverterTypeCrypterError.self) { + + crypter_b428_encrypt( + FfiConverterString.lower(plaintext), + FfiConverterString.lower(secret), + FfiConverterString.lower(nonce), $0) +} + ) +} + + + +public func decrypt(ciphertext: String, secret: String) throws -> String { + return try FfiConverterString.lift( + try + + rustCallWithError(FfiConverterTypeCrypterError.self) { + + crypter_b428_decrypt( + FfiConverterString.lower(ciphertext), + FfiConverterString.lower(secret), $0) +} + ) +} + + + +/** + * Top level initializers and tear down methods. + * + * This is generated by uniffi. + */ +public enum CrypterLifecycle { + /** + * Initialize the FFI and Rust library. This should be only called once per application. + */ + func initialize() { + } +} \ No newline at end of file diff --git a/crypter-ffi/src/crypterFFI.h b/crypter-ffi/src/crypterFFI.h new file mode 100644 index 0000000..9b07ab8 --- /dev/null +++ b/crypter-ffi/src/crypterFFI.h @@ -0,0 +1,76 @@ +// This file was autogenerated by some hot garbage in the `uniffi` crate. +// Trust me, you don't want to mess with it! + +#pragma once + +#include +#include + +// The following structs are used to implement the lowest level +// of the FFI, and thus useful to multiple uniffied crates. +// We ensure they are declared exactly once, with a header guard, UNIFFI_SHARED_H. +#ifdef UNIFFI_SHARED_H + // We also try to prevent mixing versions of shared uniffi header structs. + // If you add anything to the #else block, you must increment the version suffix in UNIFFI_SHARED_HEADER_V4 + #ifndef UNIFFI_SHARED_HEADER_V4 + #error Combining helper code from multiple versions of uniffi is not supported + #endif // ndef UNIFFI_SHARED_HEADER_V4 +#else +#define UNIFFI_SHARED_H +#define UNIFFI_SHARED_HEADER_V4 +// ⚠️ Attention: If you change this #else block (ending in `#endif // def UNIFFI_SHARED_H`) you *must* ⚠️ +// ⚠️ increment the version suffix in all instances of UNIFFI_SHARED_HEADER_V4 in this file. ⚠️ + +typedef struct RustBuffer +{ + int32_t capacity; + int32_t len; + uint8_t *_Nullable data; +} RustBuffer; + +typedef int32_t (*ForeignCallback)(uint64_t, int32_t, RustBuffer, RustBuffer *_Nonnull); + +typedef struct ForeignBytes +{ + int32_t len; + const uint8_t *_Nullable data; +} ForeignBytes; + +// Error definitions +typedef struct RustCallStatus { + int8_t code; + RustBuffer errorBuf; +} RustCallStatus; + +// ⚠️ Attention: If you change this #else block (ending in `#endif // def UNIFFI_SHARED_H`) you *must* ⚠️ +// ⚠️ increment the version suffix in all instances of UNIFFI_SHARED_HEADER_V4 in this file. ⚠️ +#endif // def UNIFFI_SHARED_H + +RustBuffer crypter_b428_derive_shared_secret( + RustBuffer their_pubkey,RustBuffer my_secret_key, + RustCallStatus *_Nonnull out_status + ); +RustBuffer crypter_b428_encrypt( + RustBuffer plaintext,RustBuffer secret,RustBuffer nonce, + RustCallStatus *_Nonnull out_status + ); +RustBuffer crypter_b428_decrypt( + RustBuffer ciphertext,RustBuffer secret, + RustCallStatus *_Nonnull out_status + ); +RustBuffer ffi_crypter_b428_rustbuffer_alloc( + int32_t size, + RustCallStatus *_Nonnull out_status + ); +RustBuffer ffi_crypter_b428_rustbuffer_from_bytes( + ForeignBytes bytes, + RustCallStatus *_Nonnull out_status + ); +void ffi_crypter_b428_rustbuffer_free( + RustBuffer buf, + RustCallStatus *_Nonnull out_status + ); +RustBuffer ffi_crypter_b428_rustbuffer_reserve( + RustBuffer buf,int32_t additional, + RustCallStatus *_Nonnull out_status + ); diff --git a/crypter-ffi/src/crypterFFI.modulemap b/crypter-ffi/src/crypterFFI.modulemap new file mode 100644 index 0000000..c87fd8b --- /dev/null +++ b/crypter-ffi/src/crypterFFI.modulemap @@ -0,0 +1,6 @@ +// This file was autogenerated by some hot garbage in the `uniffi` crate. +// Trust me, you don't want to mess with it! +framework module crypterFFI { + header "crypterFFI.h" + export * +} \ No newline at end of file