I'm trying to hash a String in Swift 3 (and/or objective C) using ripemd160
to emulate the output of the following php:
$string = 'string';
$key = 'test';
hash_hmac('ripemd160', $string, $key);
// outputs: 37241f2513c60ae4d9b3b8d0d30517445f451fa5
So far:
I've tried looking into hash_hmac
functionality that I can find, such as:
Implementing HMAC and SHA1 encryption in swift
enum HMACAlgorithm {
case MD5, SHA1, SHA224, SHA256, SHA384, SHA512, RIPEMD160
func toCCHmacAlgorithm() -> CCHmacAlgorithm {
var result: Int = 0
switch self {
case .MD5:
result = kCCHmacAlgMD5
case .SHA1:
result = kCCHmacAlgSHA1
case .SHA224:
result = kCCHmacAlgSHA224
case .SHA256:
result = kCCHmacAlgSHA256
case .SHA384:
result = kCCHmacAlgSHA384
case .SHA512:
result = kCCHmacAlgSHA512
case .RIPEMD160:
result = kCCHmacAlgRIPEMD160
}
return CCHmacAlgorithm(result)
}
func digestLength() -> Int {
var result: CInt = 0
switch self {
case .MD5:
result = CC_MD5_DIGEST_LENGTH
case .SHA1:
result = CC_SHA1_DIGEST_LENGTH
case .SHA224:
result = CC_SHA224_DIGEST_LENGTH
case .SHA256:
result = CC_SHA256_DIGEST_LENGTH
case .SHA384:
result = CC_SHA384_DIGEST_LENGTH
case .SHA512:
result = CC_SHA512_DIGEST_LENGTH
}
return Int(result)
}
}
extension String {
func hmac(algorithm: HMACAlgorithm, key: String) -> String {
let cKey = key.cString(using: String.Encoding.utf8)
let cData = self.cString(using: String.Encoding.utf8)
var result = [CUnsignedChar](repeating: 0, count: Int(algorithm.digestLength()))
CCHmac(algorithm.toCCHmacAlgorithm(), cKey!, Int(strlen(cKey!)), cData!, Int(strlen(cData!)), &result)
let hmacData:NSData = NSData(bytes: result, length: (Int(algorithm.digestLength())))
let hmacBase64 = hmacData.base64EncodedString(options: NSData.Base64EncodingOptions.lineLength76Characters)
return String(hmacBase64)
}
}
and to implement:
let hmacResult: String = "myStringToHMAC".hmac(algorithm: HMACAlgorithm.SHA1, key: "foo")
This enables me to successfully hash a string using hmac and one of the encryption algorithms provided by <CommonCrypto/CommonHMAC.h>
Issue
The issue is that library doesn't provide support for ripemd160
, so how can I add this support in to be able to accomplish what I need?
There is a question exactly the same as this but in Java here, you can see how that was solved and exactly what I'm looking for but this time using Swift 3:
Hash a string in Java emulating the php function hash_hmac using ripemd160 with a key
Here is a (self-contained) pure Swift 5 implementation of the RIPEMD-160 hash function and the corresponding HMAC-RIPEMD-160 message authentication code. It has been tested with all test vectors on the RIPEMD-160 page (tested on macOS, 32-bit iOS, 64-bit iOS).
The code for Swift 3 can be found in the edit history.
This is a translation of the reference implementation
rmd160.h,
rmd160.c
from the RIPEMD-160 page from C to Swift. I translated the helper macros and the compress()
function (the "heart" of the algorithm) as "verbatim" as possible. This allowed me to
copy/paste large C code segments and make only minor adjustments for Swift.
The update()
and finalize()
methods provide a streaming interface, similar to that
of the CommonCrypto functions.
RIPEMD160.swift:
import Foundation
public struct RIPEMD160 {
private var MDbuf: (UInt32, UInt32, UInt32, UInt32, UInt32)
private var buffer: Data
private var count: Int64 // Total # of bytes processed.
public init() {
MDbuf = (0x67452301, 0xefcdab89, 0x98badcfe, 0x10325476, 0xc3d2e1f0)
buffer = Data()
count = 0
}
private mutating func compress(_ X: UnsafePointer<UInt32>) {
// *** Helper functions (originally macros in rmd160.h) ***
/* ROL(x, n) cyclically rotates x over n bits to the left */
/* x must be of an unsigned 32 bits type and 0 <= n < 32. */
func ROL(_ x: UInt32, _ n: UInt32) -> UInt32 {
return (x << n) | ( x >> (32 - n))
}
/* the five basic functions F(), G() and H() */
func F(_ x: UInt32, _ y: UInt32, _ z: UInt32) -> UInt32 {
return x ^ y ^ z
}
func G(_ x: UInt32, _ y: UInt32, _ z: UInt32) -> UInt32 {
return (x & y) | (~x & z)
}
func H(_ x: UInt32, _ y: UInt32, _ z: UInt32) -> UInt32 {
return (x | ~y) ^ z
}
func I(_ x: UInt32, _ y: UInt32, _ z: UInt32) -> UInt32 {
return (x & z) | (y & ~z)
}
func J(_ x: UInt32, _ y: UInt32, _ z: UInt32) -> UInt32 {
return x ^ (y | ~z)
}
/* the ten basic operations FF() through III() */
func FF(_ a: inout UInt32, _ b: UInt32, _ c: inout UInt32, _ d: UInt32, _ e: UInt32, _ x: UInt32, _ s: UInt32) {
a = a &+ F(b, c, d) &+ x
a = ROL(a, s) &+ e
c = ROL(c, 10)
}
func GG(_ a: inout UInt32, _ b: UInt32, _ c: inout UInt32, _ d: UInt32, _ e: UInt32, _ x: UInt32, _ s: UInt32) {
a = a &+ G(b, c, d) &+ x &+ 0x5a827999
a = ROL(a, s) &+ e
c = ROL(c, 10)
}
func HH(_ a: inout UInt32, _ b: UInt32, _ c: inout UInt32, _ d: UInt32, _ e: UInt32, _ x: UInt32, _ s: UInt32) {
a = a &+ H(b, c, d) &+ x &+ 0x6ed9eba1
a = ROL(a, s) &+ e
c = ROL(c, 10)
}
func II(_ a: inout UInt32, _ b: UInt32, _ c: inout UInt32, _ d: UInt32, _ e: UInt32, _ x: UInt32, _ s: UInt32) {
a = a &+ I(b, c, d) &+ x &+ 0x8f1bbcdc
a = ROL(a, s) &+ e
c = ROL(c, 10)
}
func JJ(_ a: inout UInt32, _ b: UInt32, _ c: inout UInt32, _ d: UInt32, _ e: UInt32, _ x: UInt32, _ s: UInt32) {
a = a &+ J(b, c, d) &+ x &+ 0xa953fd4e
a = ROL(a, s) &+ e
c = ROL(c, 10)
}
func FFF(_ a: inout UInt32, _ b: UInt32, _ c: inout UInt32, _ d: UInt32, _ e: UInt32, _ x: UInt32, _ s: UInt32) {
a = a &+ F(b, c, d) &+ x
a = ROL(a, s) &+ e
c = ROL(c, 10)
}
func GGG(_ a: inout UInt32, _ b: UInt32, _ c: inout UInt32, _ d: UInt32, _ e: UInt32, _ x: UInt32, _ s: UInt32) {
a = a &+ G(b, c, d) &+ x &+ 0x7a6d76e9
a = ROL(a, s) &+ e
c = ROL(c, 10)
}
func HHH(_ a: inout UInt32, _ b: UInt32, _ c: inout UInt32, _ d: UInt32, _ e: UInt32, _ x: UInt32, _ s: UInt32) {
a = a &+ H(b, c, d) &+ x &+ 0x6d703ef3
a = ROL(a, s) &+ e
c = ROL(c, 10)
}
func III(_ a: inout UInt32, _ b: UInt32, _ c: inout UInt32, _ d: UInt32, _ e: UInt32, _ x: UInt32, _ s: UInt32) {
a = a &+ I(b, c, d) &+ x &+ 0x5c4dd124
a = ROL(a, s) &+ e
c = ROL(c, 10)
}
func JJJ(_ a: inout UInt32, _ b: UInt32, _ c: inout UInt32, _ d: UInt32, _ e: UInt32, _ x: UInt32, _ s: UInt32) {
a = a &+ J(b, c, d) &+ x &+ 0x50a28be6
a = ROL(a, s) &+ e
c = ROL(c, 10)
}
// *** The function starts here ***
var (aa, bb, cc, dd, ee) = MDbuf
var (aaa, bbb, ccc, ddd, eee) = MDbuf
/* round 1 */
FF(&aa, bb, &cc, dd, ee, X[ 0], 11)
FF(&ee, aa, &bb, cc, dd, X[ 1], 14)
FF(&dd, ee, &aa, bb, cc, X[ 2], 15)
FF(&cc, dd, &ee, aa, bb, X[ 3], 12)
FF(&bb, cc, &dd, ee, aa, X[ 4], 5)
FF(&aa, bb, &cc, dd, ee, X[ 5], 8)
FF(&ee, aa, &bb, cc, dd, X[ 6], 7)
FF(&dd, ee, &aa, bb, cc, X[ 7], 9)
FF(&cc, dd, &ee, aa, bb, X[ 8], 11)
FF(&bb, cc, &dd, ee, aa, X[ 9], 13)
FF(&aa, bb, &cc, dd, ee, X[10], 14)
FF(&ee, aa, &bb, cc, dd, X[11], 15)
FF(&dd, ee, &aa, bb, cc, X[12], 6)
FF(&cc, dd, &ee, aa, bb, X[13], 7)
FF(&bb, cc, &dd, ee, aa, X[14], 9)
FF(&aa, bb, &cc, dd, ee, X[15], 8)
/* round 2 */
GG(&ee, aa, &bb, cc, dd, X[ 7], 7)
GG(&dd, ee, &aa, bb, cc, X[ 4], 6)
GG(&cc, dd, &ee, aa, bb, X[13], 8)
GG(&bb, cc, &dd, ee, aa, X[ 1], 13)
GG(&aa, bb, &cc, dd, ee, X[10], 11)
GG(&ee, aa, &bb, cc, dd, X[ 6], 9)
GG(&dd, ee, &aa, bb, cc, X[15], 7)
GG(&cc, dd, &ee, aa, bb, X[ 3], 15)
GG(&bb, cc, &dd, ee, aa, X[12], 7)
GG(&aa, bb, &cc, dd, ee, X[ 0], 12)
GG(&ee, aa, &bb, cc, dd, X[ 9], 15)
GG(&dd, ee, &aa, bb, cc, X[ 5], 9)
GG(&cc, dd, &ee, aa, bb, X[ 2], 11)
GG(&bb, cc, &dd, ee, aa, X[14], 7)
GG(&aa, bb, &cc, dd, ee, X[11], 13)
GG(&ee, aa, &bb, cc, dd, X[ 8], 12)
/* round 3 */
HH(&dd, ee, &aa, bb, cc, X[ 3], 11)
HH(&cc, dd, &ee, aa, bb, X[10], 13)
HH(&bb, cc, &dd, ee, aa, X[14], 6)
HH(&aa, bb, &cc, dd, ee, X[ 4], 7)
HH(&ee, aa, &bb, cc, dd, X[ 9], 14)
HH(&dd, ee, &aa, bb, cc, X[15], 9)
HH(&cc, dd, &ee, aa, bb, X[ 8], 13)
HH(&bb, cc, &dd, ee, aa, X[ 1], 15)
HH(&aa, bb, &cc, dd, ee, X[ 2], 14)
HH(&ee, aa, &bb, cc, dd, X[ 7], 8)
HH(&dd, ee, &aa, bb, cc, X[ 0], 13)
HH(&cc, dd, &ee, aa, bb, X[ 6], 6)
HH(&bb, cc, &dd, ee, aa, X[13], 5)
HH(&aa, bb, &cc, dd, ee, X[11], 12)
HH(&ee, aa, &bb, cc, dd, X[ 5], 7)
HH(&dd, ee, &aa, bb, cc, X[12], 5)
/* round 4 */
II(&cc, dd, &ee, aa, bb, X[ 1], 11)
II(&bb, cc, &dd, ee, aa, X[ 9], 12)
II(&aa, bb, &cc, dd, ee, X[11], 14)
II(&ee, aa, &bb, cc, dd, X[10], 15)
II(&dd, ee, &aa, bb, cc, X[ 0], 14)
II(&cc, dd, &ee, aa, bb, X[ 8], 15)
II(&bb, cc, &dd, ee, aa, X[12], 9)
II(&aa, bb, &cc, dd, ee, X[ 4], 8)
II(&ee, aa, &bb, cc, dd, X[13], 9)
II(&dd, ee, &aa, bb, cc, X[ 3], 14)
II(&cc, dd, &ee, aa, bb, X[ 7], 5)
II(&bb, cc, &dd, ee, aa, X[15], 6)
II(&aa, bb, &cc, dd, ee, X[14], 8)
II(&ee, aa, &bb, cc, dd, X[ 5], 6)
II(&dd, ee, &aa, bb, cc, X[ 6], 5)
II(&cc, dd, &ee, aa, bb, X[ 2], 12)
/* round 5 */
JJ(&bb, cc, &dd, ee, aa, X[ 4], 9)
JJ(&aa, bb, &cc, dd, ee, X[ 0], 15)
JJ(&ee, aa, &bb, cc, dd, X[ 5], 5)
JJ(&dd, ee, &aa, bb, cc, X[ 9], 11)
JJ(&cc, dd, &ee, aa, bb, X[ 7], 6)
JJ(&bb, cc, &dd, ee, aa, X[12], 8)
JJ(&aa, bb, &cc, dd, ee, X[ 2], 13)
JJ(&ee, aa, &bb, cc, dd, X[10], 12)
JJ(&dd, ee, &aa, bb, cc, X[14], 5)
JJ(&cc, dd, &ee, aa, bb, X[ 1], 12)
JJ(&bb, cc, &dd, ee, aa, X[ 3], 13)
JJ(&aa, bb, &cc, dd, ee, X[ 8], 14)
JJ(&ee, aa, &bb, cc, dd, X[11], 11)
JJ(&dd, ee, &aa, bb, cc, X[ 6], 8)
JJ(&cc, dd, &ee, aa, bb, X[15], 5)
JJ(&bb, cc, &dd, ee, aa, X[13], 6)
/* parallel round 1 */
JJJ(&aaa, bbb, &ccc, ddd, eee, X[ 5], 8)
JJJ(&eee, aaa, &bbb, ccc, ddd, X[14], 9)
JJJ(&ddd, eee, &aaa, bbb, ccc, X[ 7], 9)
JJJ(&ccc, ddd, &eee, aaa, bbb, X[ 0], 11)
JJJ(&bbb, ccc, &ddd, eee, aaa, X[ 9], 13)
JJJ(&aaa, bbb, &ccc, ddd, eee, X[ 2], 15)
JJJ(&eee, aaa, &bbb, ccc, ddd, X[11], 15)
JJJ(&ddd, eee, &aaa, bbb, ccc, X[ 4], 5)
JJJ(&ccc, ddd, &eee, aaa, bbb, X[13], 7)
JJJ(&bbb, ccc, &ddd, eee, aaa, X[ 6], 7)
JJJ(&aaa, bbb, &ccc, ddd, eee, X[15], 8)
JJJ(&eee, aaa, &bbb, ccc, ddd, X[ 8], 11)
JJJ(&ddd, eee, &aaa, bbb, ccc, X[ 1], 14)
JJJ(&ccc, ddd, &eee, aaa, bbb, X[10], 14)
JJJ(&bbb, ccc, &ddd, eee, aaa, X[ 3], 12)
JJJ(&aaa, bbb, &ccc, ddd, eee, X[12], 6)
/* parallel round 2 */
III(&eee, aaa, &bbb, ccc, ddd, X[ 6], 9)
III(&ddd, eee, &aaa, bbb, ccc, X[11], 13)
III(&ccc, ddd, &eee, aaa, bbb, X[ 3], 15)
III(&bbb, ccc, &ddd, eee, aaa, X[ 7], 7)
III(&aaa, bbb, &ccc, ddd, eee, X[ 0], 12)
III(&eee, aaa, &bbb, ccc, ddd, X[13], 8)
III(&ddd, eee, &aaa, bbb, ccc, X[ 5], 9)
III(&ccc, ddd, &eee, aaa, bbb, X[10], 11)
III(&bbb, ccc, &ddd, eee, aaa, X[14], 7)
III(&aaa, bbb, &ccc, ddd, eee, X[15], 7)
III(&eee, aaa, &bbb, ccc, ddd, X[ 8], 12)
III(&ddd, eee, &aaa, bbb, ccc, X[12], 7)
III(&ccc, ddd, &eee, aaa, bbb, X[ 4], 6)
III(&bbb, ccc, &ddd, eee, aaa, X[ 9], 15)
III(&aaa, bbb, &ccc, ddd, eee, X[ 1], 13)
III(&eee, aaa, &bbb, ccc, ddd, X[ 2], 11)
/* parallel round 3 */
HHH(&ddd, eee, &aaa, bbb, ccc, X[15], 9)
HHH(&ccc, ddd, &eee, aaa, bbb, X[ 5], 7)
HHH(&bbb, ccc, &ddd, eee, aaa, X[ 1], 15)
HHH(&aaa, bbb, &ccc, ddd, eee, X[ 3], 11)
HHH(&eee, aaa, &bbb, ccc, ddd, X[ 7], 8)
HHH(&ddd, eee, &aaa, bbb, ccc, X[14], 6)
HHH(&ccc, ddd, &eee, aaa, bbb, X[ 6], 6)
HHH(&bbb, ccc, &ddd, eee, aaa, X[ 9], 14)
HHH(&aaa, bbb, &ccc, ddd, eee, X[11], 12)
HHH(&eee, aaa, &bbb, ccc, ddd, X[ 8], 13)
HHH(&ddd, eee, &aaa, bbb, ccc, X[12], 5)
HHH(&ccc, ddd, &eee, aaa, bbb, X[ 2], 14)
HHH(&bbb, ccc, &ddd, eee, aaa, X[10], 13)
HHH(&aaa, bbb, &ccc, ddd, eee, X[ 0], 13)
HHH(&eee, aaa, &bbb, ccc, ddd, X[ 4], 7)
HHH(&ddd, eee, &aaa, bbb, ccc, X[13], 5)
/* parallel round 4 */
GGG(&ccc, ddd, &eee, aaa, bbb, X[ 8], 15)
GGG(&bbb, ccc, &ddd, eee, aaa, X[ 6], 5)
GGG(&aaa, bbb, &ccc, ddd, eee, X[ 4], 8)
GGG(&eee, aaa, &bbb, ccc, ddd, X[ 1], 11)
GGG(&ddd, eee, &aaa, bbb, ccc, X[ 3], 14)
GGG(&ccc, ddd, &eee, aaa, bbb, X[11], 14)
GGG(&bbb, ccc, &ddd, eee, aaa, X[15], 6)
GGG(&aaa, bbb, &ccc, ddd, eee, X[ 0], 14)
GGG(&eee, aaa, &bbb, ccc, ddd, X[ 5], 6)
GGG(&ddd, eee, &aaa, bbb, ccc, X[12], 9)
GGG(&ccc, ddd, &eee, aaa, bbb, X[ 2], 12)
GGG(&bbb, ccc, &ddd, eee, aaa, X[13], 9)
GGG(&aaa, bbb, &ccc, ddd, eee, X[ 9], 12)
GGG(&eee, aaa, &bbb, ccc, ddd, X[ 7], 5)
GGG(&ddd, eee, &aaa, bbb, ccc, X[10], 15)
GGG(&ccc, ddd, &eee, aaa, bbb, X[14], 8)
/* parallel round 5 */
FFF(&bbb, ccc, &ddd, eee, aaa, X[12] , 8)
FFF(&aaa, bbb, &ccc, ddd, eee, X[15] , 5)
FFF(&eee, aaa, &bbb, ccc, ddd, X[10] , 12)
FFF(&ddd, eee, &aaa, bbb, ccc, X[ 4] , 9)
FFF(&ccc, ddd, &eee, aaa, bbb, X[ 1] , 12)
FFF(&bbb, ccc, &ddd, eee, aaa, X[ 5] , 5)
FFF(&aaa, bbb, &ccc, ddd, eee, X[ 8] , 14)
FFF(&eee, aaa, &bbb, ccc, ddd, X[ 7] , 6)
FFF(&ddd, eee, &aaa, bbb, ccc, X[ 6] , 8)
FFF(&ccc, ddd, &eee, aaa, bbb, X[ 2] , 13)
FFF(&bbb, ccc, &ddd, eee, aaa, X[13] , 6)
FFF(&aaa, bbb, &ccc, ddd, eee, X[14] , 5)
FFF(&eee, aaa, &bbb, ccc, ddd, X[ 0] , 15)
FFF(&ddd, eee, &aaa, bbb, ccc, X[ 3] , 13)
FFF(&ccc, ddd, &eee, aaa, bbb, X[ 9] , 11)
FFF(&bbb, ccc, &ddd, eee, aaa, X[11] , 11)
/* combine results */
MDbuf = (MDbuf.1 &+ cc &+ ddd,
MDbuf.2 &+ dd &+ eee,
MDbuf.3 &+ ee &+ aaa,
MDbuf.4 &+ aa &+ bbb,
MDbuf.0 &+ bb &+ ccc)
}
public mutating func update(data: Data) {
var X = [UInt32](repeating: 0, count: 16)
var pos = data.startIndex
var length = data.count
// Process remaining bytes from last call:
if buffer.count > 0 && buffer.count + length >= 64 {
let amount = 64 - buffer.count
buffer.append(data[..<amount])
X.withUnsafeMutableBytes {
_ = buffer.copyBytes(to: $0)
}
compress(X)
pos += amount
length -= amount
}
// Process 64 byte chunks:
while length >= 64 {
X.withUnsafeMutableBytes {
_ = data[pos..<pos+64].copyBytes(to: $0)
}
compress(X)
pos += 64
length -= 64
}
// Save remaining unprocessed bytes:
buffer = data[pos...]
count += Int64(data.count)
}
public mutating func finalize() -> Data {
var X = [UInt32](repeating: 0, count: 16)
/* append the bit m_n == 1 */
buffer.append(0x80)
X.withUnsafeMutableBytes {
_ = buffer.copyBytes(to: $0)
}
if (count & 63) > 55 {
/* length goes to next block */
compress(X)
X = [UInt32](repeating: 0, count: 16)
}
/* append length in bits */
let lswlen = UInt32(truncatingIfNeeded: count)
let mswlen = UInt32(UInt64(count) >> 32)
X[14] = lswlen << 3
X[15] = (lswlen >> 29) | (mswlen << 3)
compress(X)
buffer = Data()
let result = [MDbuf.0, MDbuf.1, MDbuf.2, MDbuf.3, MDbuf.4]
return result.withUnsafeBytes { Data($0) }
}
}
And here are "one-shot" functions to compute the hash of a message (given as Data
or as String
):
RIPEMD160-Ext.swift:
import Foundation
public extension RIPEMD160 {
static func hash(message: Data) -> Data {
var md = RIPEMD160()
md.update(data: message)
return md.finalize()
}
static func hash(message: String) -> Data {
return RIPEMD160.hash(message: message.data(using: .utf8)!)
}
}
Finally, the corresponding message authentication code. The algorithm is taken from the pseudo-code at Wikipedia: Hash-based message authentication code:
RIPEMD160-HMAC.swift:
import Foundation
public extension RIPEMD160 {
static func hmac(key: Data, message: Data) -> Data {
var key = key
key.count = 64 // Truncate to 64 bytes or fill-up with zeros.
let outerKeyPad = Data(key.map { $0 ^ 0x5c })
let innerKeyPad = Data(key.map { $0 ^ 0x36 })
var innerMd = RIPEMD160()
innerMd.update(data: innerKeyPad)
innerMd.update(data: message)
var outerMd = RIPEMD160()
outerMd.update(data: outerKeyPad)
outerMd.update(data: innerMd.finalize())
return outerMd.finalize()
}
static func hmac(key: Data, message: String) -> Data {
return RIPEMD160.hmac(key: key, message: message.data(using: .utf8)!)
}
static func hmac(key: String, message: String) -> Data {
return RIPEMD160.hmac(key: key.data(using: .utf8)!, message: message)
}
}
To print the message digest as a hex-encoded string, the following method from How to convert Data to hex string in swift can be used:
extension Data {
func hexEncodedString() -> String {
return map { String(format: "%02hhx", $0) }.joined()
}
}
Example 1:
let msg = "Hello World"
let hash = RIPEMD160.hash(message: msg).hexEncodedString()
print(hash) // a830d7beb04eb7549ce990fb7dc962e499a27230
Example 2 (your example):
let hmac = RIPEMD160.hmac(key: "test", message: "string").hexEncodedString()
print(hmac) // 37241f2513c60ae4d9b3b8d0d30517445f451fa5
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With